Giter Club home page Giter Club logo

blog's Introduction

Hi there 👋

Charged and Ready

Be interested in these art

JavaScript Vue React Node TypeScript Golang Docker Flutter Deno Rust

This is Jwchan/飘香豆腐 from China, base on Guangzhou.

  • 🌱 I’m currently learning vue3, typescript
  • 😄 Expect: Raise skills && Live better
  • 📫 You can find me on QQ 741755613

blog mini-vue PPAP.live mini-vue-router PPAP.server PPAP.admin

blog's People

Contributors

jwchan1996 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

qiumingx

blog's Issues

docker gitlab 备份还原

docker gitlab 备份还原

灾难起源

起因是服务器 root 文件系统内存较小,只有 50G,经常爆仓。
于是乎,就把 gitlab 整体移动到内存相对较大 home 文件系统下。
这不,转移了,我人直接裂开来。

噩耗!!!

我打开命令窗口,口嚼香糖,一顿蜻蜓点水,在键盘上滑过 cp -R /app /home,刹那间,整整几个 G 的文件便搬了家。当然,其中包含着这个星期来所备份的文件。

然而,此时的我仍然在享受着香糖在口腔带来的愉悦,却不知下一秒,gitlab 的明天与意外哪个会先来。

我对着 gitlab 满心许下祝福,轻轻地敲下:

$ cd /home/app/docker/gitlab
$ docker-compose up

而此刻,等待我的却是,无尽的无尽。

gitaly cant up

有个 gitaly 服务启动失败!

失格!!!

啊,这……

在重新尝试多次启动无果后,似乎走上了一条不归路。
漆黑的命令窗口,如同深渊,凝视着你的凝视。

随着:

$ docker rm `docker ps -a | grep Exited | awk '{print $1}'`  
$ docker rmi -f `docker images | grep '<none>' | awk '{print $3}'`  

命令的执行,似乎变得清净起来。
清除了停止的容器与无用的镜像。干净的环境,总会为 gitlab 带来好运吧。然而,到头来还是一场梦。

依然启动不了 gitaly 服务,而带来的后果,就是仓库页面数据读取的失败。

gitlab 503

其他页面功能正常,仓库页面访问返回 503

返璞!!!

既然是转移之后出现的错误,那我再转移回来会报错吗?

随着我把命令喂给窗口后,gitlab 回到了原本属于它的家乡。正如同古诗句“乡音无改鬓毛衰”,此刻的 gitlab 与 它生长的 /app 环境却那么的格格不入。

依然是启动不了 gitaly,那么,gitaly 启动失败跟仓库页面 503 有什么关系呢?

Gitaly 是一个 Git RPC
服务,用于处理 GitLab 进行的所有 git 调用。
后台服务,专门负责访问磁盘以高效处理 git 操作,并缓存耗时操作。所有 git 操作都通过 Gitaly 处理。

即是 gitalygitlab 使用的处理读取 git 的桥梁服务,gitaly 开不起来,那么 gitlabgit 仓库数据的读取便是失了效。

归真!!!

经过一番查找,定位问题为数据卷挂载目录下 /app/volumes/gitlab/gitlab/repositoies/ 这个文件夹。

执行 ls,列出目录下文件:

+gitaly @hashed  @pools

有三个文件,试着把 +gitaly 文件夹删掉,重新启动 docker gitlab,发现会重新生成 +gitaly 文件夹,此时的 gitlab,好像一个捉迷藏玩的很好的孩子,藏了很久终于被发现了一样。

仓库页面已经可以打开了,除了代码仓库显示为空仓库,其他数据也可以读取了。

空仓库

执行 ls -a,大家都摊牌,别藏着掖着了:

$ ls -a
+gitaly .gitaly-metadata  @hashed  @pools

发现有一个隐藏的文件 .gitaly-metadata,查看 cat .gitaly-metadata

{"gitaly_filesystem_id":"c19d98bb-9bf7-4579-b120-c6e33902c225"}

估计就是这个生成的配置文件系统 ID 指向失效了,导致找不到 git 仓库地址。

尝试过去查找 gitaly_filesystem_id 的意义,发现源码是 ruby 函数生成,精力有限,遂无深究。

人祸湮灭

时间旅行

没错,就是恢复备份。

执行

$ cd /app/docker/gitlab
$ docker-compose run --rm gitlab app:rake gitlab:backup:restore

执行中,会让选择恢复的备份文件,输入备份 tar 包完整名称,回车即可。
如:1593450065_2020_06_29_13.0.6_gitlab_backup.tar

但是,很不幸,出现权限问题。

还原出现权限问题
还原失败

精准夺权

遇到阻碍,那就夺权。

目标是备份文件 tar 包下所有文件的用户组与所有者,都修改为 root 用户。

把备份文件 tar 包拖出来,解压到 backup 文件夹,修改权限到 root 用户组与 root 所有者。

备份文件解压

修改 backup 文件夹里面所有文件为用户组 root

chgrp -R root backup

修改 backup 文件夹里面所有文件为所有者 root

chown -R root backup

然后将夺权后的文件再重新打包为 tar 包。

继续执行:

$ cd /app/docker/gitlab
$ docker-compose run --rm gitlab app:rake gitlab:backup:restore

还原过程

虽然还有报权限错误,因为 tar.gz 包里面没有夺权,但是问题不大。

柳暗花明

回到 docker-compose.yml 目录,启动容器!

$ cd /app/docker/gitlab
$ docker-compose up

柳暗花明又一村,我胡汉三又回来了!

启动成功

qiankun 微前端应用实践与部署(三)

qiankun 微前端应用实践与部署(三)

微前端架构下,主应用有自己的路由模块,同时主应用支持以微前端的方式嵌入业务模块(子应用),如何实现呢?

关于路由

qiankun 在主应用初始化会自动监听路由的变化去匹配注册好的子应用路由活动规则,同时 vue 路由也会监听路由变化。

因为主应用有自己的业务模块,需要支持页面刷新,所以采用 hash 路由模式。qiankun 官方 demo 使用的是 history 路由模式。

那么,采用 hash 路由模式的话,主应用路由会有 /#/ 的前缀,比如主应用的 resource 组件路由:

http://localhost:8889/#/resource

假设 history 路由模式下子应用的注册信息为:

{
  name: 'live',
  entry: '//localhost:7102',
  container: '#subapp-viewport',
  activeRule: '/live',
}

此时 qiankun 只有命中 urlhttp://localhost:8889/live 才会加载子应用。

此处假设使用的路由切换代码为:

this.$router.push({
  path: '/live'
})

所以现在切换的 urlhttp://localhost:8889/#/live,显然不能匹配 /live,所以加载子应用失败。我们需要修改一下子应用注册的 activeRule,使得匹配 hash 路由模式。

为了区分开主应用的自身模块与子应用的路由区别,子应用的路由增加 /micro 前缀,比如 /micro/live 是子应用的路由。

那么 hash 路由模式下子应用的注册信息变成:

{
  name: 'live',
  entry: '//localhost:7102',
  container: '#subapp-viewport',
  activeRule: '/#/micro/live',
}

这样的话,主应用路由切换后的 url 就能命中子应用的 activeRule 了。

同时,子应用也需要将路由模式设置为 hash 模式,否则,会出现在子应用切换自身路由时,改变主应用 hash 路由的情况。比如子应用切换自身路由 /about,此时 url 会变成 http://localhost:8889/about/#/micro/live,导致路由命中失败。我们期望的 urlhttp://localhost:8889/#/micro/live/about

所以,为了兼容主应用的 hash 模式路由,子应用也需要设置为 hash 模式的路由,最终结果是实现子应用路由与子应用注册在主应用的 activeRule 的一致性。

下面会分别对主应用与子应用进行配置。

配置子应用路由

子应用是常规 vue 项目,需要做调整的的是路由配置文件 /router/index.js 以及入口文件 main.js

// router/index.js

let prefix = ''

// 判断是 qiankun 环境则增加路由前缀
if(window.__POWERED_BY_QIANKUN__){
  prefix = '/micro/live'
}

const routes = [
  {
    path: prefix + '/',
    name: 'home',
    component: Home,
  },
  {
    path: prefix +'/about',
    name: 'about',
    component: About
  },
]
// main.js

let router = null;
let instance = null;

function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    // 默认为 hash 路由模式
    // base: window.__POWERED_BY_QIANKUN__ ? '/micro/live' : '/',
    // mode: 'history',
    routes,
  })

  // 判断 qiankun 环境则进行路由拦截,判断跳转路由是否有 /micro 开头前缀,没有则加上
  if(window.__POWERED_BY_QIANKUN__){
    router.beforeEach((to, from, next) => {
      if(!to.path.includes('/micro')){
        next({
          path: '/micro/live' + to.path
        })
      }else{
        next()
      }
    })
  }

  instance = new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

配置主应用路由

主应用需要修改的是子应用注册的路由匹配规则,因为主应用采用的是 hash 路由模式,qiankun 需要命中路由的话,activeRule 需要带上 /#/ 前缀。

// App.vue

const apps = [
  {
    name: 'live',
    entry: '//localhost:7101',
    container: '#subapp-viewport',
    activeRule: '/#/micro/live',
  }
]

registerMicroApps(apps, 
  {
    beforeLoad: [
      app => {
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
      },
    ],
    beforeMount: [
      app => {
        console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
      },
    ],
    afterUnmount: [
      app => {
        console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
      },
    ],
  },
)
// router/index.js

// httpRoutes 非必要,主要用于匹配 vue 应用路由,与路由通配符 {path: '*'} 一起使用
// 因为如果 vue 路由没有匹配,默认是加载 Home 组件的
// 这样 vue 路由视图会与子应用共存,不符合业务需求
//
// 当前 httpRoutes 的路由配置是没有设置 path 对应的组件,所以匹配的路由视图必为空
//
// 如果不设置路由通配符,则 httpRoutes 不需要配置
const httpRoutes = [
  {
    path: '/micro/live',
    name: 'Live'
    // 没有配置 component,则 router-view 不会加载组件
  },
  {
    path: '/micro/live/:microRoute',
    name: 'Live*'
    // 没有配置 component,则 router-view 不会加载组件
  }
]

const router = new VueRouter({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    ...httpRoutes,
    {
      path: '*',
      component: Home
    }
  ]
})

url 变化时,首先主应用进入的是 qiankun 的路由匹配规则,匹配到 /#/micro/live 时,会加载子应用,同时主应用的 vue 路由匹配到路由后不会加载路由组件,这样就达到只显示子应用而 vue 路由组件不显示的目的。

当子应用内部的 <router-link to="/about"> 被点击时,首先子应用跳转路由前会加上 /micro/live 前缀,实际上是 /micro/live/about,匹配到 about 路由,然后在主应用的 vue 路由中匹配到 /micro/live/:microRoute,同样不会加载组件。

本文参考:前端微服务(qiankun)哈希路由实践

往期系列文章

qiankun 微前端应用实践与部署

qiankun 微前端应用实践与部署(二)

qiankun 微前端应用实践与部署(三)

Git 代码托管

Git 代码托管

前置条件: 在 github 上新建仓库,本地与远端已实现身份认证

🔶 第一步: 初始化 git 仓库(本地)

$ git init

🔶 第二步: 将项目所有文件添加到待提交列表(缓冲区)中

$ git add .

🔶 第三步: 将 add 的文件 commit 到本地仓库(并添加注释)

$ git commit -m "注释语句"

🔶 第四步: 将本地仓库关联到 github 远程仓库

$ git remote add origin https://github.com/jwchan1996/pizza-app.git    #仓库地址

🔶 第五步: 将代码上传到 github 远程仓库(推送)

# 使用 -u 代表指定默认主机,这样以后就可以不加任何参数使用 git pull 与 git push
$ git push -u origin master   

完成上述步骤之后,之后的代码提交基本是以下四步:

$ git status
$ git add .
$ git commit -m "[feat] new feat"
$ git push

🌖 注意: 多人协作每次同步代码需要先将代码拉下来(拉取)

👉 首先将代码提交到本地仓库

$ git add .
$ git commit -m "注释语句"

👉 将远程代码拉取下来

$ git pull origin master

👉 然后再将本地仓库代码上传

$ git push origin master

至此,github 代码托管就完成啦(๑>؂<๑)

🌘 附: Git基本常用命令

$ mkdir XX    #创建一个空目录 XX指目录名

$ pwd     #显示当前目录的路径。

$ git init    #把当前的目录变成可以管理的git仓库,生成隐藏.git文件。

$ git add XX    #把xx文件添加到暂存区去。

$ git commit –m “XX”   #提交文件 –m 后面的是注释。

$ git status    #查看仓库状态

$ git diff  XX    #查看XX文件修改了那些内容

$ git log    #查看历史记录

$ git reset --hard HEAD^ 或者 git reset --hard HEAD~  #回退到上一个版本

$ git reset –hard HEAD~100   #回退到100个版本

$ cat XX    #查看XX文件内容

$ git reflog    #查看历史记录的版本号id

$ git checkout -- XX    #把XX文件在工作区的修改全部撤销。

$ git rm XX    #删除XX文件

$ git remote add origin https://github.com/jwchan1996/pizza-app.git    #关联一个远程库

$ git push –u(第一次要用-u 以后不需要) origin master    #把当前master分支推送到远程库

$ git clone https://github.com/jwchan1996/pizza-app.git    #从远程库中克隆

$ git checkout –b dev    #创建dev分支 并切换到dev分支上

$ git branch    #查看当前所有的分支

$ git checkout master    #切换回master分支

$ git merge dev   #在当前的分支上合并dev分支

$ git branch –d dev   #删除dev分支

$ git branch name    #创建分支

$ git stash    #把当前的工作隐藏起来 等以后恢复现场后继续工作

$ git stash list   #查看所有被隐藏的文件列表

$ git stash apply   #恢复被隐藏的文件,但是内容不删除

$ git stash drop   #删除文件

$ git stash pop   #恢复文件的同时 也删除文件

$ git remote    #查看远程库的信息

$ git remote –v    #查看远程库的详细信息

$ git push origin master   #Git会把master分支推送到远程库对应的远程分支上

关于 ref 与 $refs 对 dom 元素的操作

关于 ref 与 $refs 对 dom 元素的操作

如何获取 v-for 渲染的多个 ref 的 dom

🍍 在编写 vue 项目过程中,遇到了获取不到正确的 dom 元素节点的问题。

功能界面如图所示:

界面

需要实现的是,点击每个播放器的右上角的关闭按钮,则关闭播放器。
代码如下:

<template>
  <div class="device-status">
    <!-- 树组件 -->
    <Tree></Tree>
    <!-- 业务内容 -->
    <div class="content-container" ref="container">
      <h1>{{ msg }}</h1>
      <div>{{buildings}}</div>
      <div class="video-box" ref="video1">
        <Player 
                src="https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/12846619_51a524dffce6834f1d221be2a1037834.mp4"
                poster="https://puui.qpic.cn/fans_admin/0/3_118841988_1557667793407/0"
        ></Player>
        <i class="el-icon-circle-close" @click="close('video1')"></i>
      </div>
    </div>
  </div>
</template>

<script>
import Tree from "component/checkbox-tree/tree";
import Player from "component/player/player";

export default {
  data() {
    return {
      msg: "我是组件",
      videoList: [
        {
          id: 1,
          index: 0,
          src: "https://gss3.baidu.com/6LZ0ej3k1Qd3ote6lo7D0j9wehsv/tieba-smallvideo/12846619_51a524dffce6834f1d221be2a1037834.mp4",
          poster: "https://puui.qpic.cn/fans_admin/0/3_118841988_1557667793407/0"
        },
        {
          id: 2,
          index: 1,
          src: "https://cdn.theguardian.tv/webM/2015/07/20/150716YesMen_synd_768k_vp8.webm",
          poster: "https://ww1.sinaimg.cn/large/007i4MEmgy1g29h63wl0yj30et08c0tc.jpg"
        }
      ]
    };
  },
  computed: {
    buildings(){
      return this.$store.state.buildingTree
    }
  },
  components: {
    Tree,
    Player
  },
  watch: {
    buildings(newVal, oldVal){
      console.log("监听到树选中值变化",JSON.stringify(newVal))
      //判断树数组的值,空则不作操作,否则带上树id
      //进行 http 请求获取数据

    }
  },
  methods: {
    //关闭播放器
    close(videoStr){
      //应该用数据驱动dom,这里直接操作了dom,不符合vue的理念,暂时
      let video = this.$refs[videoStr]
      video.parentNode.removeChild(video)
    }
  }
};
</script>

至此代码功能正常,点击右上角关闭按钮,则移除播放器元素。

然后问题在于,播放器可能有多个存在,这时候,如何实现点击每个关闭按钮,关闭对应的播放器呢?

尝试修改代码如下:

<template v-for="video in videoList">
  <!-- 播放器组件,带关闭按钮 -->
  <div class="video-box" :ref="`video${video.id}`" >
    <Player 
           :src="video.src"
           :poster="video.poster"
    ></Player>
    <i class="el-icon-circle-close" @click="close(`video${video.id}`)"></i>
  </div>
</template>
methods: {
  //关闭播放器
  close(videoStr){
    //应该用数据驱动dom,这里直接操作了dom,不符合vue的理念,暂时
    let video = this.$refs[videoStr]
    video.parentNode.removeChild(video)
  }
}

然后点击关闭按钮时,可以看到控制台报错:

报错

提示不能获取未定义的属性,则表明该 dom 元素节点获取不对。

发现问题所在

后面经过一番折腾,发现以上代码 this.$refs[videoStr] 获取的是一个 ref 等于 videoStr (此处为变量)的 dom 节点数组,不是单个 dom 节点元素!

数组节点

至此,踩坑这个之后,就明白了为什么获取不到对应的 dom 元素了。

代码修改如下即可:

methods: {
  //关闭播放器
  close(videoStr){
    //应该用数据驱动dom,这里直接操作了dom,不符合vue的理念,暂时
    let video = this.$refs[videoStr][0]
    video.parentNode.removeChild(video)
  }
}

功能实现后,再来拓展其他方法。

比如当 ref 的值一样,都为 videoBox 时:

<template v-for="video in videoList">
  <div class="video-box" ref="videoBox" >
    <Player 
           :src="video.src"
           :poster="video.poster"
    ></Player>
    <i class="el-icon-circle-close" @click="close(video.index)"></i>
  </div>
</template>
methods: {
  //关闭播放器
  close(index){
    // 利用数组下标操作
    let video = this.$refs.videoBox[index]
    video.parentNode.removeChild(video)
  }
}

又或者不使用 ref 属性,改而为每个播放器元素赋值 id,也可:

<template v-for="video in videoList">
  <div class="video-box" :id="`video${video.id}`">
    <Player 
           :src="video.src"
           :poster="video.poster"
    ></Player>
    <i class="el-icon-circle-close" @click="close(`video${video.id}`)"></i>
  </div>
</template>
methods: {
  //关闭播放器
  close(videoId){
    // 利用数组下标操作
    let video = document.querySelector(`#${videoId}`)
    video.parentNode.removeChild(video)
  }
}

官网概念:

官网概念

总结:

ref 相当于给元素或组件赋予一个 ID 引用,用来注册引用信息的,方便获取 dom 元素或获取组件实例。

使用场景:

1. ref 加在普通元素上,this.$refs.name 获取的是 dom 元素
2. ref 加在子组件上,this.$refs.name 获取到的是组件实例,方便父组件使用子组件的所有方法
3. 当 v-for 用于元素或组件,ref 获取的将是一组数组或 dom 节点

文章所说遇到的问题即是上述第三种情况。

qiankun 微前端应用实践与部署

qiankun 微前端应用实践与部署

微前端应用分为主应用与子应用,部署方式是分别编译好主应用与子应用,将主应用与子应用部署到 nginx 配置好的目录即可。

代码仓库 https://github.com/jwchan1996/qiankun-micro-app

分别进入 portalapp1app2 根目录,执行:

开发模式

# portal
yarn
yarn start
# app1、app2
npm install
npm run dev

生产模式

# portal
yarn build
# app1、app2
npm run build

主应用

主应用 js 文件引入 qiankun 注册子应用,并编写导航页显示跳转逻辑。

<!DOCTYPE html>
<html lang="zh">

<head>
  <meta charset="UTF-8">
  <title>QianKun Example</title>
</head>

<body>
  <div class="mainapp">
    <!-- 标题栏 -->
    <header class="mainapp-header">
      <h1>导航</h1>
    </header>
    <div class="mainapp-main">
      <!-- 侧边栏 -->
      <ul class="mainapp-sidemenu">
        <li class="app1">应用一</li>
        <li class="app2">应用二</li>
      </ul>
      <!-- 子应用  -->
      <main id="subapp-container"></main>
    </div>
  </div>

  <script src="./index.js"></script>
</body>

</html>

主应用 js 入口文件:

import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun';
import './index.less';

/**
 * 主应用 **可以使用任意技术栈**
 * 以下分别是 React 和 Vue 的示例,可切换尝试
 */
import render from './render/ReactRender';
// import render from './render/VueRender';

/**
 * Step1 初始化应用(可选)
 */
render({ loading: true });

const loader = loading => render({ loading });

/**
 * Step2 注册子应用
 */
registerMicroApps(
  [
    {
      name: 'app1',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app1',
    },
    {
      name: 'app2',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7101' : '//localhost:7101',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app2',
    }
  ],
  {
    beforeLoad: [
      app => {
        console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
      },
    ],
    beforeMount: [
      app => {
        console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
      },
    ],
    afterUnmount: [
      app => {
        console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
      },
    ],
  },
);

const { onGlobalStateChange, setGlobalState } = initGlobalState({
  user: 'qiankun',
});

onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev));

setGlobalState({
  ignore: 'master',
  user: {
    name: 'master',
  },
});

/**
 * Step3 设置默认进入的子应用
 */
// setDefaultMountApp('/app1');

/**
 * Step4 启动应用
 */
start();

runAfterFirstMounted(() => {
  console.log('----------------------------------')
  console.log(process.env.NODE_ENV)
  console.log('----------------------------------')
  console.log('[MainApp] first app mounted');
});

//浏览器地址入栈
function push(subapp) { history.pushState(null, subapp, subapp) }

//配合导航页显示逻辑
function initPortal(){
  //主应用跳转
  document.querySelector('.app1').onclick = () => {
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
    push('/app1')
  }
  document.querySelector('.app2').onclick = () => {
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
    push('/app2')
  }

  //回到导航页
  document.querySelector('.mainapp-header h1').onclick = () => {
    push('/')
  }

  if(location.pathname !== '/'){
    document.querySelector('.mainapp-sidemenu').style.visibility = 'hidden'
  }else{
    document.querySelector('.mainapp-sidemenu').style.visibility = 'visible'
  }
  if(location.pathname.indexOf('login') > -1){
    document.querySelector('.mainapp-header').style.display = 'block'
  }else{
    document.querySelector('.mainapp-header').style.display = 'none'
  }

  //监听浏览器前进回退
  window.addEventListener('popstate', () => { 
    if(location.pathname === '/'){
      document.querySelector('.mainapp-sidemenu').style.visibility = 'visible'
    }
    if(location.pathname.indexOf('login') > -1){
      document.querySelector('.mainapp-header').style.display = 'block'
    }else{
      document.querySelector('.mainapp-header').style.display = 'none'
    }
  }, false)
}

initPortal()

docker nginx 配置

此处 nginx 主要作用是用于端口目录转发,并配置主应用访问子应用的跨域问题。

使用 docker 配置部署 nginx

# docker-compose.yml

version: '3.1'
services:
  nginx:
    restart: always
    image: nginx
    container_name: nginx
    ports: 
      - 8888:80
      - 8889:8889
      - 7100:7100
      - 7101:7101
    volumes: 
      - /app/volumes/nginx/nginx.conf:/etc/nginx/nginx.conf
      - /app/volumes/nginx/html:/usr/share/nginx/html
      - /app/micro/portal:/app/micro/portal
      - /app/micro/app1:/app/micro/app1
      - /app/micro/app2:/app/micro/app2

将编译后的主应用以及子应用放到对应的数据卷挂载目录即可,如主应用 /app/micro/portal
同理,也需要将配置好的 nginx.conf 文件放到指定的数据卷挂载目录,使用 docker-compose up -d 启动即可。

nginx 端口目录转发配置:

# nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;

    server {
      listen	8889;
      server_name 192.168.2.192;
      
      location / {
        root /app/micro/portal;
        index index.html;
        
        try_files $uri $uri/ /index.html;
      }
    }

    server {
      listen	7100;
      server_name 192.168.2.192;
      
      # 配置跨域访问,此处是通配符,严格生产环境的话可以指定为主应用 192.168.2.192:8889
      add_header Access-Control-Allow-Origin *;
      add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
      add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
      
      location / {
        root /app/micro/app1;
        index index.html;
        
        try_files $uri $uri/ /index.html;
      }
    }

    server {
      listen	7101;
      server_name 192.168.2.192;
      
      # 配置跨域访问,此处是通配符,严格生产环境的话可以指定为主应用 192.168.2.192:8889
      add_header Access-Control-Allow-Origin *;
      add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
      add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
      
      location / {
        root /app/micro/app2;
        index index.html;
        
        try_files $uri $uri/ /index.html;
      }
    }
}

子应用适配框架

下面子应用以常规 vue 项目为例。

入口文件 main.js

在入口文件增加 qiankun 环境判断,判断当前是 qiankuan 环境的则将子应用引入到主应用框架内,然后在主框架内执行正常的 vue 元素挂载。

// 在所有代码的文件之前引入判断
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

import Vue from "vue";
import App from "./App";
import router from "./router";

let instance = null;

function render(props = {}) {
  // 此处 container 是主应用生成的用于装载子应用的 div 元素
  // 如 <div id="__qiankun_microapp_wrapper_for_app_1_1596504716562__" />
  const { container } = props;
  
  instance = new Vue({
    router,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

function storeTest(props) {
  props.onGlobalStateChange &&
    props.onGlobalStateChange(
      (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
      true,
    );
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name,
      },
    });
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
  console.log('[vue] props from main framework', props);
  storeTest(props);
  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}

router 配置

路由需要根据 qiankun 环境配置 base 路径,以及设置路由的 history 模式。

// router/index.js
const router = new Router({
  // 此处 /app1 是子应用在主应用注册的 activeRule
  base: window.__POWERED_BY_QIANKUN__ ? '/app1' : '/',
  mode: 'history',
  routes: [
    {
        ……
        ……
    }
  ]
})

// portal/index.js
registerMicroApps(
  [
    {
      name: 'app1',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app1',
    }
  ]
)

子应用打包

打包 umd 格式

output: {
    library: 'portal',
    libraryTarget: 'umd'
}

字体图标与 css 背景图片路径问题

默认情况下,在 css 引用的资源使用 url-loader 加载打包出来是相对路径的,所以会出现子应用的资源拼接到主应用的 domain 的情况,造成加载资源失败。

因为 element-ui 的字体图标是在 css 里面引入的,还有相关背景图片的引入也是在 css 里,所以需要配置 webpackurl-loader,生产模式情况下直接指定资源前缀。

module: {
  rules: [
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("img/[name].[hash:7].[ext]"),
        //这里 192.168.2.192:7100 是子应用部署地址
        publicPath: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : ''
      }
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("fonts/[name].[hash:7].[ext]"),
        publicPath: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7101' : ''
      }
    }
  ]
}

koa 实现 token 有效时间续期的思路

koa 实现 token 有效时间续期的思路

场景描述

🍭 在前后端的交互中,无可避免的存在一些需要验证身份的请求。
🍭 一般来说,使用 token 作为身份令牌可以实现身份验证的问题。
🍭 以此同时,token 作为自带描述信息的无状态数据,唯一的判断标准就是生成 token 时设置的有效期时间,当超过有效期时则作废。
🍭 我们在使用 APPWEB 应用时,如果正在操作的时候,token 刚好过期了,那就出大问题了。
🍭 所有的数据请求都会失败,给用户带来极其糟糕的体验。
🍭 所以,如何才能让 token 进行续期呢?

分析思路

🍤 因为 token 是无状态数据,一旦生成了,不能主动删除或者让它失效,唯一的就是等待有效期时间到。
🍤 所以,我们会想到,在 token 过期时客户端携带新的 token 来访问数据接口,是不是就可以了呢。
🍤 答案是的,那么现在需要解决的问题就是:

1.怎么返回新的 token 给到客户端
2.什么时候返回 token 使得用户登录状态得到续期

处理思路

  1. 通过设置返回头设置新 token 的值,客户端使用 axios 进行响应拦截判断是否有新 token 字段,有则保存起来。
  2. 如果用户在一定的 token 有效时间段期间(比如有效期时间的后半段)访问了数据接口,就应该对 token 进行续期。

代码实现

🍥 本次项目采用的是 koa 基于 node 实现的 api 服务端。
🍥 主要文件有两个,一个是入口文件 app.js,另一个是工具函数文件 token.js
🍥 完整 github 项目可以查看 PPAP.server

//token.js

const jwt = require('jsonwebtoken')
const secret = require('../config/config').secret

//判断token是否应该更新有效期(续期)
const getTokenRenewStatus = () => {

  //检测当前token是否到达续期时间段
  let obj = parseToken()
  if(!obj.email){
    return false
  }
  //更新时间段在过期前3天
  if(obj.exp - new Date().getTime()/1000 > 60*60*24*3){
    return false
  }else{
    return true
  }

}

//获取一个期限为7天的token
const getToken = (payload = {}) => {
  return jwt.sign(payload, secret, { expiresIn: 60*60*24*7 })
}

//重新生成一个期限为7天的token
const createNewToken = () => {

  let token = global.token
  let obj = jwt.verify(token, secret)
  let payload = {
    uid: obj.uid,
    name: obj.name,
    account: obj.account,
    roleId: obj.roleId,
    email: obj.email,
    password: obj.password
  }
  return getToken(payload)

}

//解析token为对象
const parseToken = () => {
  
  let token = global.token
  try {
    return jwt.verify(token, secret)
  }catch {
    console.log('token is expired')
    return {}
  }
  
}

module.exports = {
  secret,
  getTokenRenewStatus,
  getToken,
  createNewToken,
  parseToken
}
//app.js

const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const jwtKoa = require('koa-jwt')  // 用于路由权限控制
const app = new Koa()

const config = require('./config/config')

const tokenUtil = require('./util/token')
const router = require('./router')

const jwtUnless = require('./util/jwt_unless')  //用于判断是否需要jwt验证

//配置ctx.body解析中间件
app.use(bodyParser())

// 错误处理
app.use((ctx, next) => {
  //设置CORS跨域
  ctx.set("Access-Control-Allow-Origin", "*")
  ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE")
  ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type, Authorization")
  ctx.set("Content-Type", "application/json;charset=utf-8")
  ctx.set("Access-Control-Expose-Headers", "new_token")
  //获取token,保存全局变量
  if(ctx.request.header.authorization){
    global.token = ctx.request.header.authorization.split(' ')[1]
    //检测当前token是否到达续期时间段
    let obj = tokenUtil.parseToken()
    //解析token携带的信息
    global.uid = obj.uid
    global.name = obj.name
    global.account = obj.account
    global.email = obj.email
    global.roleId = obj.roleId
    //先解析全局变量再执行next(),保证函数实时获取到变量值
  }
  return next().then(() => {
    //执行完下面中间件后进入
    //判断不需要jwt验证的接口,跳过token续期判断
    if(jwtUnless.checkIsNonTokenApi(ctx)) return
    //判断token是否应该续期(有效时间)
    if(tokenUtil.getTokenRenewStatus()){
      //设置header
      ctx.set({
        new_token: tokenUtil.createNewToken()
      })
    }
  }).catch((err) => {
      //携带token的Authorization参数错误
      if(err.status === 401){
          ctx.status = 200
          ctx.body = {
            status: 401,
            message: '未携带token令牌或者token令牌已过期'
          }
      }else{
          throw err
      }
  })
})

//配置不需要jwt验证的接口
app.use(jwtKoa({ secret: tokenUtil.secret }).unless({
  //自定义过滤函数,详细参考koa-unless
  custom: ctx => {
    if(jwtUnless.checkIsNonTokenApi(ctx)){
      //是不需要验证token的接口
      return true
    }else{
      //是需要验证token的接口
      return false
    }
  }
}));

//初始化路由中间件
app.use(router.routes()).use(router.allowedMethods())

//监听启动窗口
app.listen(config.port, () => console.log(`PPAP.server is run on ${config.host}:${config.port}`))

vue 数据与视图更新

vue 数据与视图更新

场景

vue 数据 data 更新了,但是视图没有更新
其中缘由在于对 vue 的响应式原理的理解偏差

追踪变化

把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

数据更新

声明响应性属性

Vue 中,一般只有在 data 选项中声明的属性(或者是属性的属性)才是具有响应特性的。如果需要在 data 选项之外对已有属性添加具有响应特性的属性,需要用到 Vueset 方法。

var vm = new Vue({
  data: {
    a: {              //a就是根级属性,不可动态添加
      b: 0          //b就是属性的属性,可以动态添加
    }
  }
})

Vue 不允许动态添加根级响应式属性,必须在初始化实例前声明根级响应式属性,哪怕只是一个空值:

var vm = new Vue({
  data: {
    // 声明 message 为一个空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'

何为响应特性?就是当我们更改 data 中的值的时候,HTML 与之绑定的部分会随之更新的特性。

数组更新检测

变异方法替换数组

Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。这些方法如下:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

打开控制台,然后用 items 数组调用变异方法:example1.items.push({ message: 'Baz' })

非变异方法替换数组

变异方法 (mutation method),顾名思义,会改变被这些方法调用的原始数组。相比之下,也有非变异 (non-mutating method) 方法,例如:filter(), concat()slice() 。这些不会改变原始数组,但总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组:

example1.items = example1.items.filter(function (item) {
  return item.message.match(/Foo/)
})

你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

注意事项

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个栗子:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将触发状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)

你也可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set 的一个别名:

vm.$set(vm.items, indexOfItem, newValue)

为了解决第二类问题,你可以使用 splice

vm.items.splice(newLength)

对象更新检测

还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除:

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

你可以添加一个新的 age 属性到嵌套的 userProfile 对象:

Vue.set(vm.userProfile, 'age', 27)

你还可以使用 vm.$set 实例方法,它只是全局 Vue.set 的别名:

vm.$set(vm.userProfile, 'age', 27)

有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign()_.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

Object.assign(vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

你应该这样做:

vm.userProfile = Object.assign({}, vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

数据响应式的几个例子

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应的

vm.b = 2
// `vm.b` 是非响应的

a 就是在 data 中声明的具有响应特性的属性,而 b 就不是。

var vm = new Vue({
  data: {
    a: {
      a1:''
    }
  },
  methods: {
    change:function(){
      this.a.a1 = "text1"    //a1就是响应式的
      this.a.a2 = "text2"    //a2就不是响应式的
    }
  }
})

a2 虽然不是响应式的,但它却是可以在 HTML 部分被渲染更新出来。这里就是一个比较容易掉进去的坑。由于 Vue 是异步执行 DOM 更新,虽然更新的动作是由 this.a.a1 = "text1" 触发,可动作的完成是在 this.a.a2 = "text2" 之后,下面有详细解析。

var vm = new Vue({
  data: {
    a: {
      a1:''
    }
  },
  methods: {
    change: function () {
      this.a.a1 = "text1"    //a1就是响应式的
      this.a.a2 = "text2"    //a2就不是响应式的
      var that = this;
      setTimeout(function () {
        that.a.a3 = 'new text'; //这里与a2是相同的,区别在于这里的a3并不会被渲染到DOM中
        that.$set(that.a, 'a4', 'new text');    //这是正确的添加属性的方法
        that.a = {                              //这种写法与a2不同,a5可以被更新到DOM中
          a5: 'hahaha'
        }
      }, 300);
    }
  }
})

异步更新带来的数据响应式误解

异步数据的处理基本是一定会遇到的,处理不好就会遇到数据不更新的问题,但有一种情况是在未正确处理的情况下也能正常更新,这就会造成一种误解,详情如下所示:

new Vue({
    el: '#app',
    data: {
        dataObj: {}
    },
    ready: function () {
        var self = this;

        /**
         * 异步请求模拟
         */
        setTimeout(function () {
            self.dataObj = {}; 
            self.dataObj['text'] = 'new text';
        }, 3000);
    }
})

上面的代码非常简单,我们都知道 vue 中在 data 里面声明的数据才具有响应式的特性,所以我们一开始在 data 中声明了一个 dataObj 空对象,然后在异步请求中执行了两行代码,如下:

self.dataObj = {}; 
self.dataObj['text'] = 'new text';

首先清空原始数据,然后添加一个 text 属性并赋值。到这里为止一切都如我们所想的,数据和模板都更新了。

那么问题来了,dataObj.text 具有响应式的特性吗?

模板更新了,应该具有响应式特性,如果这么想那么你就已经走入了误区,一开始我们并没有在 data 中声明 .text 属性,所以该属性是不具有响应式的特性的。

但模板切切实实已经更新了,这又是怎么回事呢?

那是因为 vuedom 更新是异步的,即当 setter 操作发生后,指令并不会立马更新,指令的更新操作会有一个延迟,当指令更新真正执行的时候,此时 .text 属性已经赋值,所以指令更新模板时得到的是新值。
具体流程如下所示:

  • self.dataObj = { }; 发生 setter 操作
  • vue 监测到 setter 操作,通知相关指令执行更新操作
  • self.dataObj['text'] = 'new text'; 赋值语句
  • 指令更新开始执行

所以真正的触发更新操作是 self.dataObj = { }; 这一句引起的,所以单看上述例子,具有响应式特性的数据只有 dataObj 这一层,它的子属性是不具备的。

注:其实 vue 文档中已经有说明,对于新增以及删除的属性,vue 是无法监测到的。

var a = {};

a.b = 0;    //新增b属性
a = {
    c: 0
};              //更改a属性的值

上述两种赋值方式对 vue 造成的影响是不同的。

对比示例:

new Vue({
    el: '#app',
    data: {
        dataObj: {}
    },
    ready: function () {
        var self = this;

        /**
         * 异步请求模拟
         */
        setTimeout(function () {
            self.dataObj['text'] = 'new text';
        }, 3000);
    }
})

上述例子的模板是不会更新的。

🍭 Vue.$set

通过 $set 方法可以将添加一个具备响应式特性的属性,并且其子属性也具备响应式特性,但是必须是新属性才可以,如果是本身已有的属性该方法是不起作用的。

new Vue({
    el: '#app',
    data: {
        dataObj: {}
    },
    ready: function () {
        var self = this;

        /**
         * 异步请求模拟
         */
        setTimeout(function () {
            var data = {
                name: 'xiaofu',
                age: 18
            };
            var data01 = {
                name: 'yangxiaofu',
                age: 19
            };
            self.dataObj['person'] = {};
            self.$set('dataObj.info', data);
            self.$set('dataObj.person', data01); 
        }, 3000);
    }
})

如上所示,.person 属性是不具备响应式特性的。

版本库提交信息规范与自动验证

版本库提交信息规范与自动验证

版本库提交信息规范

以下规范是社区使用最广的 Angular 规范,稍加修改。

一般提交命令我们使用如 git commit- m "feat(user): add user login"

其中当前 commit message 信息包括三部分:type(必需)、scope(可选)、 subject(必需)

type

type 用于说明 commit 的类别,允许使用下面 8 个标识。

feat: # 新功能(feature)

fix: # 修补 bug

docs: # 文档(documentation)

style: # 格式(不影响代码执行的变动,非css样式专用)

refactor: # 重构(既不是新增功能,也不是修改 bug 的代码变动)

test: # 增加测试

chore: # 构建过程或辅助工具的变动

revert: # 对之前修改代码 commit 记录的还原

scope

scope 用于说明 commit 的影响范围,比如数据层、控制层、视图层、功能模块等,视项目的不同而不同。

例如:

  • 新增用户管理的新增用户功能
$ git commit -m "feat(user): add addUser"
  • 修复了用户管理的新增用户功能的bug
$ git commit -m "fix(user): addUser"
  • 更新文档说明
$ git commit -m "docs(readme): update readme"
  • 调整新增用户代码格式(代码换行或者删除换行)
$ git commit -m "style(user/add): wrap line"
  • 增加单元测试代码
$ git commit -m "test: add test"
  • 配置或者修改自动化部署文件或者构建工具
$ git commit -m "chore: cicd"

$ git commit -m "chore: add vue-router"

还有一种是相对来说比较特殊的对之前 commit 代码的回退,比如还原之前修改 bug 的错误提交

$ git commit -m "revert: fix(user): addUser"

subject

subjectcommit 目的的简短描述,不超过 50 个字符

$ git commit -m "feat(course): 完成课程管理模块"

issue_id and pr_id

github 还提供了自动识别 issuepr 链接的功能。
在提交信息后面加上对应的 issue_idpr_id,能够自动识别为对应链接,点击可跳转到对应界面。

$ git commit -m "fix(user): check user pwd (#1)"

这样的话,会自动识别 #1 为链接,跳到对应的 issue 或者 pr 页面。

自动验证提交信息规范

为了达到提交信息规范的校验效果,我们使用配置本地 git hooks 模板的方法,只需设置一次,之后从githubclone 下来的代码都会沿用当前 hooks 配置。

下面命令操作环境为 git bash

  • github 克隆仓库到本地
$ git clone https://github.com/jwchan1996/commit-msg.git
$ cd /commit-msg
  • 创建 git 钩子模板文件夹
$ mkdir -p ~/.git_template/hooks 
  • 复制当前配置好的 commit-msg hooks 文件到模板
$ cp commit-msg ~/.git_template/hooks
  • 设置 git 的初始化模板的使用模板
$ git config --global init.templatedir ~/.git_template 

具体 commit-msg 文件可以点击查看详情

SVG 基础

SVG 基础

具体页面效果可以查看 svg 基础

svg 语法

svg 标签

SVG 代码都放在顶层标签 svg 之中。

<svg width="100%" height="100%">
  <circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

SVG 的 width 属性和 height 属性,指定了 SVG 图像在 HTML 元素中所占据的宽度和高度。除了相对单位,也可以采用绝对单位(单位:像素)。如果不指定这两个属性,SVG 图像默认大小是 300 像素(宽) x 150 像素(高)。

如果只想展示 SVG 图像的一部分,就要指定 viewBox 属性。

<svg width="100" height="100" viewBox="50 50 50 50">
  <circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

viewBox 属性的值有四个数字,分别是左上角的横坐标和纵坐标、视口的宽度和高度。上面代码中,SVG 图像是 100 像素宽 x 100 像素高,viewBox 属性指定视口从 (50, 50) 这个点开始。所以,实际看到的是右下角的四分之一圆。

注意,视口必须适配所在的空间。上面代码中,视口的大小是 50 x 50,由于 SVG 图像的大小是 100 x 100,所以视口会放大去适配 SVG 图像的大小,即放大了四倍。

如果不指定 width 属性和 height 属性,只指定 viewBox 属性,则相当于只给定 SVG 图像的长宽比。这时,SVG 图像的默认大小将等于所在的 HTML 元素的大小。

circle 圆形 样式

<svg width="300" height="180">
  <circle cx="30"  cy="50" r="25" />
  <circle cx="90"  cy="50" r="25" class="red" />
  <circle cx="150" cy="50" r="25" class="fancy" />
</svg>

上面的代码定义了三个圆。circle 标签的 cx、cy、r 属性分别为横坐标、纵坐标和半径,单位为像素。坐标都是相对于 svg 画布的左上角原点。

class 属性用来指定对应的 CSS 类。

.red {
  fill: red;
}

.fancy {
  fill: none;
  stroke: black;
  stroke-width: 3pt;
}

SVG 的 CSS 属性与网页元素有所不同。

fill:填充色
stroke:描边色
stroke-width:边框宽度

line 直线

<svg width="300" height="50">
  <line x1="0" y1="25" x2="200" y2="25" style="stroke:rgb(0,0,0);stroke-width:5" />
</svg>

上面代码中,line 标签的 x1 属性和 y1 属性,表示线段起点的横坐标和纵坐标;x2 属性和 y2 属性,表示线段终点的横坐标和纵坐标;style 属性表示线段的样式。

polyline 折线

polyline 的 points 属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔

<svg width="300" height="100">
  <polyline points="3,50 30,28 40,0 50,10 60,30 70,40 80,0 90,80 100,60 110,60" fill="none" stroke="red" />
</svg>

rect 矩形

<svg width="300" height="100">
  <rect x="0" y="0" height="80" width="160" style="stroke: #70d5dd; fill: #dd524b" />
</svg>

rect 的 x 属性和 y 属性,指定了矩形左上角端点的横坐标和纵坐标;width 属性和 height 属性指定了矩形的宽度和高度(单位像素)。

ellipse 椭圆

<svg width="300" height="80">
  <ellipse cx="60" cy="40" ry="20" rx="40" stroke="black" stroke-width="5" fill="silver"/>
</svg>

ellipse 的 cx 属性和 cy 属性,指定了椭圆中心的横坐标和纵坐标(单位像素);rx 属性和 ry 属性,指定了椭圆横向轴和纵向轴的半径(单位像素)。

polygon 多边形

<svg width="300" height="100">
  <polygon fill="green" stroke="orange" stroke-width="1" points="0,0 100,0 150,50 100,100 0,100 0,0"/>
</svg>

polygon 的 points 属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔。

path 路径

<svg width="300" height="80">
  <path d="
    M 18,3
    L 46,3
    L 46,40
    L 61,40
    L 32,68
    L 3,40
    L 18,40
    Z
  "></path>
</svg>

path 的 d 属性表示绘制顺序,它的值是一个长字符串,每个字母表示一个绘制动作,后面跟着坐标。

M:移动到(moveto)
L:画直线到(lineto)
Z:闭合路径

text 文本

<svg width="300" height="40">
  <text x="20" y="25">Hello World</text>
</svg>

text 的 x 属性和 y 属性,表示文本区块基线(baseline)起点的横坐标和纵坐标。文字的样式可以用 class 或 style 属性指定。

use 复制一个形状

<svg width="300" viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
  <circle id="myCircle3" cx="5" cy="5" r="4"/>

  <use href="#myCircle3" x="10" y="0" fill="#009688" />
  <use href="#myCircle3" x="20" y="0" fill="white" stroke="#f2aa24" />
</svg>

use 的 href 属性指定所要复制的节点,x 属性和 y 属性是 use 左上角的坐标。另外,还可以指定 width 和 height 坐标。

g 分组

g 标签用于将多个形状组成一个组(group),方便复用。

<svg width="300" height="100">
  <g id="myCircle">
    <text x="25" y="20">圆形</text>
    <circle cx="50" cy="50" r="20"/>
  </g>

  <use href="#myCircle" x="100" y="0" fill="blue" />
  <use href="#myCircle" x="200" y="0" fill="white" stroke="blue" />
</svg>

defs 自定义形状

defs 标签用于自定义形状,它内部的代码不会显示,仅供引用

<svg width="300" height="100">
  <defs>
    <g id="myCircle">
      <text x="25" y="20">圆形</text>
      <circle cx="50" cy="50" r="20"/>
    </g>
  </defs>

  <use href="#myCircle" x="0" y="0" />
  <use href="#myCircle" x="100" y="0" fill="#bc3545" />
  <use href="#myCircle" x="200" y="0" fill="white" stroke="#bc3545" />
</svg>

pattern 自定义形状

pattern 标签用于自定义一个形状,该形状可以被引用来平铺一个区域。

<svg width="500" height="200">
  <defs>
    <pattern id="dots" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
      <circle fill="#bee9e8" cx="50" cy="50" r="35" />
    </pattern>
  </defs>
  <rect x="0" y="0" width="100%" height="100%" fill="url(#dots)" />
</svg>

上面代码中,pattern 标签将一个圆形定义为 dots 模式。patternUnits="userSpaceOnUse" 表示 pattern 的宽度和长度是实际的像素值。然后,指定这个模式去填充下面的矩形。

image 插入图片

<svg viewBox="0 0 100 100" width="150" height="80">
  <image xlink:href="https://jwchan.cn/logo.png" width="90%" height="90%"/>
</svg>

上面代码中,image 的 xlink:href 属性表示图像的来源。

animate 动画

<svg width="500px" height="100px">
  <rect x="0" y="0" width="100" height="100" fill="#feac5e">
    <animate attributeName="x" from="0" to="500" dur="2s" repeatCount="indefinite" />
  </rect>
</svg>

上面代码中,矩形会不断移动,产生动画效果。

animate 的属性含义如下。

attributeName:发生动画效果的属性名。
from:单次动画的初始值。
to:单次动画的结束值。
dur:单次动画的持续时间。
repeatCount:动画的循环模式。

可以在多个属性上面定义动画。

<svg width="500px" height="100px">
  <rect x="0" y="0" width="100" height="100" fill="#feac5e">
    <animate attributeName="x" from="0" to="500" dur="2s" repeatCount="indefinite" />
    <animate attributeName="width" to="500" dur="2s" repeatCount="indefinite" />
  </rect>
</svg>

animateTransform 变形

animate 标签对 CSS 的 transform 属性不起作用,如果需要变形,就要使用 animateTransform 标签。

<svg width="500px" height="300px">
  <rect x="200" y="200" width="50" height="50" fill="#4bc0c8">
    <animateTransform attributeName="transform" type="rotate" begin="0s" dur="3s" from="0 200 100" to="360 300 200" repeatCount="indefinite" />
  </rect>
</svg>

上面代码中,animateTransform 的效果为旋转(rotate),这时 from 和 to 属性值有三个数字,第一个数字是角度值,第二个值和第三个值是旋转中心的坐标。from="0 200 100"表示开始时,角度为 0,围绕(200, 100)开始旋转;to="360 300 200"表示结束时,角度为 360,围绕(300, 200)旋转。

JavaScript 操作

DOM 操作

如果 SVG 代码直接写在 HTML 网页之中,它就成为网页 DOM 的一部分,可以直接用 DOM 操作。

<svg
  id="mysvg"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 800 600"
  preserveAspectRatio="xMidYMid meet"
>
  <circle id="mycircle" cx="100" cy="60" r="50" />
</svg>

上面代码插入网页之后,就可以用 CSS 定制样式。

<svg
  id="mysvg"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 800 150"
  preserveAspectRatio="xMidYMid meet"
>
  <style>
    .mycircle-1-1 { stroke-width: 5; stroke: #f00; fill: #ff0; }
    .mycircle-1-1:hover { stroke: #090; fill: #fff; cursor: pointer; }
  </style>
  <circle id="mycircle" class="mycircle-1-1" cx="100" cy="60" r="50" />
</svg>

然后,可以用 JavaScript 代码操作 SVG。

<svg
  id="mysvg"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 800 150"
  preserveAspectRatio="xMidYMid meet"
>
  <style>
    .mycircle-1-2 { stroke-width: 5; stroke: #f00; fill: #ff0; }
    .mycircle-1-2:hover { stroke: #090; fill: #fff; cursor: pointer; }
  </style>
  <circle id="mycircle" class="mycircle-1-2" cx="100" cy="60" r="50" />
  <script type="text/javascript">
    const mycircle = document.querySelector('.mycircle-1-2')
    mycircle.addEventListener('click', function(){
      mycircle.setAttribute('r', 60)
      mycircle.setAttribute('cy', 80)
    }, false)
  </script>
</svg>

上面代码指定,如果点击图形,就改写 circle 元素的 r 属性和 cy 属性。

获取 SVG DOM

使用 object、iframe、embed 标签插入 SVG 文件,可以获取 SVG DOM。

const svgObject = document.getElementById('object').contentDocument
const svgIframe = document.getElementById('iframe').contentDocument
const svgEmbed = document.getElementById('embed').getSVGDocument()

注意,如果使用 img 标签插入 SVG 文件,就无法获取 SVG DOM。

读取 SVG 源码

由于 SVG 文件就是一段 XML 文本,因此可以通过读取 XML 代码的方式,读取 SVG 源码。

<svg
  id="svg-container"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  xml:space="preserve" width="500" height="440"
>
  <rect id="svg-rect" x="0" y="0" height="80" width="160" style="stroke: #70d5dd; fill: #dd524b" />
</svg>

使用 XMLSerializer 实例的 serializeToString() 方法,获取 SVG 元素的代码。

const svgString = new XMLSerializer().serializeToString(document.querySelector('#svg-container'))

点击矩形,获取 svg 元素字符串。

参考链接 http://www.ruanyifeng.com/blog/2018/08/svg.html

手写实现一个 Promise

手写实现一个 Promise

关于 Promsie

下面是实现一个 Promise 的一些关键点:

  • Promise 是一个类,在执行这个类的时候,需要传递一个执行器(executor)进去,执行器会立即执行。
  • Promise 中有三种状态,分别为成功(fulfilled)、失败(rejected)和等待(pending)。其中 pending 状态会变为 fulfilled 或者 rejected,且一旦状态确定就不可以更改。
  • resolvereject 函数是用来改变状态的: resolve -> fulfilledreject -> rejected
  • then 方法内部做的事情就是判断状态。如果状态是成功,调用成功的回调函数。如果状态是失败,调用失败的回调函数。每一个 Promise 对象都能够调用 then 方法,then 方法是被定义在原型对象中的。
  • then 的成功回调函数有一个参数,表示成功之后的值。then 的 失败回调函数也有一个参数,表示失败后的原因。

Promise 的基本用法:

let promise = new Promise((resolve, reject) => {
    resolve('成功')
    //reject('失败')
})

promise.then(value => {
    console.log(value)
}, reason => {
    console.log(reason)
})

实现一个简单 Promise 类

const PENDING = 'pending'   // 等待
const FULFILLED = 'fulfilled'   // 成功
const REJECTED = 'rejected'     // 失败

class MyPromise {
    constructor (executor) {
        // 传入一个执行器并立即执行(执行器两个参数是函数)
        executor(this.resolve, this.reject)
    }
    
    // promise 状态初始值
    status = PENDING
    // promise 成功 resolve 传递的值(默认值)
    value = undefined
    // promise 失败 reject 传递的原因(默认值)
    reason = undefined
    
    resolve = value => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为成功
        this.status = FULFILLED
        // 保存 promise 成功传过来的值
        this.value = value 
    }
    
    reject = reason => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为失败
        this.status = REJECTED
        // 保存 promise 失败传过来的原因
        this.reason = reason 
    }
    
    // 类的所有方法都是定义在类的 prototype 属性上面的
    // 即类的方法是被定义在原型对象中的,此处 then 方法就是
    // then 方法期望的参数是回调函数
    then (successCallback, failCallback) {
        // 判断状态
        if (this.status === FULFILLED) {
            successCallback(this.value)
        } else if (this.status === REJECTED) {
            failCallback(this.reason)
        }
    }
}

Promise 类中处理异步逻辑

假设在我们在 MyPromise 类实例对象的执行器中执行异步任务 setTimeout

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('成功')
    }, 2000)
    //reject('失败')
})

promise.then(value => {
    console.log(value)
}, reason => {
    console.log(reason)
})

代码的执行顺序是从上到下依次执行的,在创建 MyPromsie 类实例对象后,执行器会立即执行。在执行器代码执行过程中,发现 setTimeout 是异步任务,那么代码执行主线程会在开启这个异步任务后会立即往下执行,不会等待异步任务的完成,所以会立即往下执行 then 方法。

但是,由于此时还没有发生状态改变,MyPromise 实例对象的状态还是处于 pending 状态,因此我们要在 then 方法内对 pending 这种状态进行处理(因为 then 方法 只执行一次,等异步任务结束后 Promise 状态改变已经不会再执行这一次的 then 方法了)。

异步耗时任务结束后会调用 resolve 或者 reject 方法,因此我们还需要在 MyPromise 类的 resolve 属性方法和 reject 属性方法内调用定义在 then 方法内的成功回调函数和失败回调函数。
那么就要在 then 方法中判断当前状态是否处于 pending,在 MyPromise 类中添加两个新属性 successCallbackfailCallback 来接收 then 方法的成功回调函数和失败回调函数,以便于在异步任务结束后能在 resolvereject 属性方法内进行调用,使 then 方法在 Promise 执行器有异步任务的情况下能正常工作。

const PENDING = 'pending'   // 等待
const FULFILLED = 'fulfilled'   // 成功
const REJECTED = 'rejected'     // 失败

class MyPromise {
    constructor (executor) {
        // 传入一个执行器并立即执行(执行器的两个参数是函数)
        executor(this.resolve, this.reject)
    }
    
    // promise 状态初始值
    status = PENDING
    // promise 成功 resolve 传递的值(默认值)
    value = undefined
    // promise 失败 reject 传递的原因(默认值)
    reason = undefined
    
    // 成功回调
    successCallback = undefined
    // 失败回调
    failCallback = undefined
    
    resolve = value => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为成功
        this.status = FULFILLED
        // 保存 promise 成功传过来的值
        this.value = value 
        
        // 判断成功回调是否存在,如果存在,则调用
        this.successCallback && this.successCallback(this.value)
    }
    
    reject = reason => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为失败
        this.status = REJECTED
        // 保存 promise 失败传过来的原因
        this.reason = reason 
        
        // 判断失败回调是否存在,如果存在,则调用
        this.failCallback && this.failCallback(this.reason)
    }
    
    // 类的所有方法都是定义在类的 prototype 属性上面的
    // 即类的方法是被定义在原型对象中的,此处 then 方法就是
    // then 方法期望的参数是回调函数
    then (successCallback, failCallback) {
        // 判断状态
        if (this.status === FULFILLED) {
            successCallback(this.value)
        } else if (this.status === REJECTED) {
            failCallback(this.reason)
        } else {
        
            // pending 等待状态
            // 如果 Promsie 实例对象传入的执行器存在异步任务时
            // 会开启异步任务后立即执行 then,此时状态还是处于 pending
            // 将成功回调和失败回调函数通过类的属性储存起来
            // 以便于异步任务结束后在 resolve 或 reject 中执行 then 的回调函数
            this.successCallback = successCallback
            this.failCallback = failCallback
            
        }
    }
}

实现 then 方法多次调用

同一个 Promise 实例对象的 then 方法是可以被多次调用的,当 Promise 状态变为成功或失败时,then 方法里面的成功或失败回调函数是要被依次调用的。其中,Promise 实例对象传入的执行器里面的代码有可能是同步执行也有可能是异步执行。

如果是同步执行,执行 then 方法的时候 Promise 状态已经改变,那么执行 then 方法内相对应的回调函数即可。如果是异步执行,那么我们需要将多次调用 then 方法所触发的回调函数用数组储存起来,等待 Promise 执行器的异步代码执行完毕后,根据返回状态成功或失败,在 resolvereject 属性方法内循环数组,依次调用(队列先进先出)回调函数即可。

let promise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('成功')
    }, 2000)
    // resolve('成功')
    // reject('失败')
})

promise.then(value => {
    console.log(value)
})
promise.then(value => {
    console.log(value)
})
const PENDING = 'pending'   // 等待
const FULFILLED = 'fulfilled'   // 成功
const REJECTED = 'rejected'     // 失败

class MyPromise {
    constructor (executor) {
        // 传入一个执行器并立即执行(执行器两个参数是函数)
        executor(this.resolve, this.reject)
    }
    
    // promise 状态初始值
    status = PENDING
    // promise 成功 resolve 传递的值(默认值)
    value = undefined
    // promise 失败 reject 传递的原因(默认值)
    reason = undefined
    
    // 成功回调
    // successCallback = undefined
    // 因为多次调用 then 方法,所以用数组来存储多个回调函数
    successCallback = []
    // 失败回调
    // failCallback = undefined
    failCallback = []
    
    resolve = value => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为成功
        this.status = FULFILLED
        // 保存 promise 成功传过来的值
        this.value = value 
        
        // 判断成功回调是否存在,如果存在,则调用
        // this.successCallback && this.successCallback(this.value)
        // 因为多次调用 then 方法,回调函数存储在数组中,所以需要循环依次调用(队列先进先出)
        while(this.successCallback.length) this.successCallback.shift()(this.value)
    }
    
    reject = reason => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为失败
        this.status = REJECTED
        // 保存 promise 失败传过来的原因
        this.reason = reason 
        
        // 判断失败回调是否存在,如果存在,则调用
        // this.failCallback && this.failCallback(this.reason)
        // 因为多次调用 then 方法,回调函数存储在数组中,所以需要循环依次调用(队列先进先出)
        while(this.failCallback.length) this.failCallback.shift()(this.reason)
    }
    
    // 类的所有方法都是定义在类的 prototype 属性上面的
    // 即类的方法是被定义在原型对象中的,此处 then 方法就是
    // then 方法期望的参数是回调函数
    then (successCallback, failCallback) {
        // 判断状态
        if (this.status === FULFILLED) {
            successCallback(this.value)
        } else if (this.status === REJECTED) {
            failCallback(this.reason)
        } else {
        
            // pending 等待状态
            // 如果 Promsie 实例对象传入的执行器存在异步任务时
            // 会开启异步任务后立即执行 then,此时状态还是处于 pending
            // 将成功回调和失败回调函数通过类的属性储存起来
            // 以便于异步任务结束后在 resolve 或 reject 中执行 then 的回调函数
            
            // 多次调用 then 方法,回调函数存储到数组中
            // this.successCallback = successCallback
            this.successCallback.push(successCallback)
            // this.failCallback = failCallback
            this.failCallback.push(failCallback)
            
        }
    }
}

实现 then 方法的链式调用

Promise 实例对象的 then 方法是可以被链式调用的。要实现 then 方法的链式调用,then 方法必须返回一个 Promise 对象。所以我们要在 then 方法里创建一个新的 Promise 对象并返回, 还要实现将上一个 then 方法的回调函数返回值传递给下一个 then 方法。

具体实现是在上一个 then 方法里返回的新 Promise 对象的执行器里执行 resolve 方法,resolve 方法会把上一个 then 方法的回调函数返回的值传递给新 Promise 对象的 then 方法,这样就实现了 then 方法的链式调用。

let promise = new MyPromise((resolve, reject) => {
    // setTimeout(() => {
    //    resolve('成功')
    // }, 2000)
    resolve('成功')
    // reject('失败')
})

promise.then(value => {
    console.log(value)
    return 100
}).then(value => {
    console.log(value)
})
const PENDING = 'pending'   // 等待
const FULFILLED = 'fulfilled'   // 成功
const REJECTED = 'rejected'     // 失败

class MyPromise {
    constructor (executor) {
        // 传入一个执行器并立即执行(执行器两个参数是函数)
        executor(this.resolve, this.reject)
    }
    
    // promise 状态初始值
    status = PENDING
    // promise 成功 resolve 传递的值(默认值)
    value = undefined
    // promise 失败 reject 传递的原因(默认值)
    reason = undefined
    
    // 成功回调
    // successCallback = undefined
    // 因为多次调用 then 方法,所以用数组来存储多个回调函数
    successCallback = []
    // 失败回调
    // failCallback = undefined
    failCallback = []
    
    resolve = value => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为成功
        this.status = FULFILLED
        // 保存 promise 成功传过来的值
        this.value = value 
        
        // 判断成功回调是否存在,如果存在,则调用
        // this.successCallback && this.successCallback(this.value)
        // 因为多次调用 then 方法,回调函数存储在数组中,所以需要循环依次调用(队列先进先出)
        while(this.successCallback.length) this.successCallback.shift()(this.value)
    }
    
    reject = reason => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为失败
        this.status = REJECTED
        // 保存 promise 失败传过来的原因
        this.reason = reason 
        
        // 判断失败回调是否存在,如果存在,则调用
        // this.failCallback && this.failCallback(this.reason)
        // 因为多次调用 then 方法,回调函数存储在数组中,所以需要循环依次调用(队列先进先出)
        while(this.failCallback.length) this.failCallback.shift()(this.reason)
    }
    
    // 类的所有方法都是定义在类的 prototype 属性上面的
    // 即类的方法是被定义在原型对象中的,此处 then 方法就是
    // then 方法期望的参数是回调函数
    then (successCallback, failCallback) {
    
        // 创建新的 Promise 对象作为返回值,实现链式调用 then 方法
        let newPromise = new MyPromise((resolve, reject) => {
        
            // 判断状态
            if (this.status === FULFILLED) {
                
                // 新的 Promise 对象的执行器调用 resolve 方法,参数是上一个 then 方法的回调函数返回值
                // 因为是实现 then 方法的链式调用,所以只在 fulfilled 状态处理即可
                let x = successCallback(this.value)
                resolve(x)
                
            } else if (this.status === REJECTED) {
                failCallback(this.reason)
            } else {
            
                // pending 等待状态
                // 如果 Promsie 实例对象传入的执行器存在异步任务时
                // 会开启异步任务后立即执行 then,此时状态还是处于 pending
                // 将成功回调和失败回调函数通过类的属性储存起来
                // 以便于异步任务结束后在 resolve 或 reject 中执行 then 的回调函数
                
                // 多次调用 then 方法,回调函数存储到数组中
                // this.successCallback = successCallback
                this.successCallback.push(successCallback)
                // this.failCallback = failCallback
                this.failCallback.push(failCallback)
                
            }
            
        })
        
        return newPromise
        
    }
}

在链式调用 then 方法时,可以在回调函数返回一个普通值,也可以返回一个 Promise 对象。

如果 then 方法的成功回调函数返回的是普通值,可以直接在 then 方法返回的新 Promise 对象的执行器中直接调用 resolve 方法,就可以把值传递给新 Promise 对象接下来要调用的 then 方法的回调函数。

如果 then 方法的成功回调函数返回的是 Promise 对象,则需要查看 Promise 对象返回的结果,再根据结果决定调用 resolve 方法还是 reject 方法。

let promise = new MyPromise((resolve, reject) => {
    // setTimeout(() => {
    //    resolve('成功')
    // }, 2000)
    resolve('成功')
    // reject('失败')
})

promise.then(value => {
    console.log(value)
    // return 100
    return Promise.resolve('other')
}).then(value => {
    console.log(value)
})
const PENDING = 'pending'   // 等待
const FULFILLED = 'fulfilled'   // 成功
const REJECTED = 'rejected'     // 失败

class MyPromise {
    constructor (executor) {
        // 传入一个执行器并立即执行(执行器两个参数是函数)
        executor(this.resolve, this.reject)
    }
    
    // promise 状态初始值
    status = PENDING
    // promise 成功 resolve 传递的值(默认值)
    value = undefined
    // promise 失败 reject 传递的原因(默认值)
    reason = undefined
    
    // 成功回调
    // successCallback = undefined
    // 因为多次调用 then 方法,所以用数组来存储多个回调函数
    successCallback = []
    // 失败回调
    // failCallback = undefined
    failCallback = []
    
    resolve = value => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为成功
        this.status = FULFILLED
        // 保存 promise 成功传过来的值
        this.value = value 
        
        // 判断成功回调是否存在,如果存在,则调用
        // this.successCallback && this.successCallback(this.value)
        // 因为多次调用 then 方法,回调函数存储在数组中,所以需要循环依次调用(队列先进先出)
        while(this.successCallback.length) this.successCallback.shift()(this.value)
    }
    
    reject = reason => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为失败
        this.status = REJECTED
        // 保存 promise 失败传过来的原因
        this.reason = reason 
        
        // 判断失败回调是否存在,如果存在,则调用
        // this.failCallback && this.failCallback(this.reason)
        // 因为多次调用 then 方法,回调函数存储在数组中,所以需要循环依次调用(队列先进先出)
        while(this.failCallback.length) this.failCallback.shift()(this.reason)
    }
    
    // 类的所有方法都是定义在类的 prototype 属性上面的
    // 即类的方法是被定义在原型对象中的,此处 then 方法就是
    // then 方法期望的参数是回调函数
    then (successCallback, failCallback) {
    
        // 创建新的 Promise 对象作为返回值,实现链式调用 then 方法
        let newPromise = new MyPromise((resolve, reject) => {
        
            // 判断状态
            if (this.status === FULFILLED) {
                
                // 新的 Promise 对象的执行器调用 resolve 方法,参数是上一个 then 方法的回调函数返回值
                // 因为是实现 then 方法的链式调用,所以只在 fulfilled 状态处理即可
                let x = successCallback(this.value)
                // 注释掉 resolve(x),因为返回值有两种
                // 判断 x 的值是普通值还是 Promise 对象
                // 如果是普通值,直接调用 resolve
                // 如果是 Promise 对象,查看 Promise 对象返回的结果
                // 再根据 Promise 对象返回的结果,决定调用 resolve 还是 reject
                resolvePromise(x, resolve, reject)
                // ↑ 抽离为一个通用函数
                
            } else if (this.status === REJECTED) {
                failCallback(this.reason)
            } else {
            
                // pending 等待状态
                // 如果 Promsie 实例对象传入的执行器存在异步任务时
                // 会开启异步任务后立即执行 then,此时状态还是处于 pending
                // 将成功回调和失败回调函数通过类的属性储存起来
                // 以便于异步任务结束后在 resolve 或 reject 中执行 then 的回调函数
                
                // 多次调用 then 方法,回调函数存储到数组中
                // this.successCallback = successCallback
                this.successCallback.push(successCallback)
                // this.failCallback = failCallback
                this.failCallback.push(failCallback)
                
            }
            
        })
        
        return newPromise
        
    }
}

function resolvePromise (x, resolve, reject) {
    // 判断 x 是否是 Promise 实例
    if (x instanceof MyPromise) {
        // Promise 对象
        // x.then(value => resolve(value), reason => reject(reason))
        // 因为 resolve 跟 reject 都是函数,且在这里是作为 then 方法的回调函数,所以可以简化为
        x.then(resolve, reject)
    } else {
        // 普通值
        resolve(x)
    }
}

then 方法链式调用识别 Promise 对象自返回

then 方法回调函数可以返回 Promise 对象,但是有一种情况是例外,那就是在 then 方法回调函数里,不能返回当前 then 方法的 Promise 对象,否则程序会抛出循环调用的错误。如:

var promise = new Promise(function (resolve, reject) {
    resolve(100)
})

var p1 = promise.then(function (value) {
    console.log(value)
    // 这样写会发生 Promise 对象的循环调用,会抛出错误
    return p1
})

p1.then(function () {}, function (reason) {
    console.log(reason.message)     // Chaining cycle detected for promise #<Promise>
})

那么,下面在我们自己实现的 MyPromise 类实现对这个错误进行捕获并提示。

首先我们需要判断 then 方法所返回的 Promise 对象是否是当前 then 方法的 Promise 对象,我们需要将 newPromise 实例对象传到 resolvePromise 函数去。

class Mypromise {
    ...
    ...
    
    then (successCallback, failCallback) {
        // 创建新的 Promise 对象作为返回值,实现链式调用 then 方法
        let newPromise = new MyPromise((resolve, reject) => {
        
            // 判断状态
            if (this.status === FULFILLED) {
                
                // 只需要判断 x 跟 newPromise 是否相等
                // 即可判断出回调函数返回的 promise 对象是否是当前 then 方法的 Promise 对象
                let x = successCallback(this.value)
                // 将 newPromise 对象传递到 resolvePromise 函数中做判断
                resolvePromise(newPromise, x, resolve, reject)
                
            } else if (this.status === REJECTED) {
                failCallback(this.reason)
            } else {
                
                // pending
                this.successCallback.push(successCallback)
                this.failCallback.push(failCallback)
                
            }
            
        })
        
        return newPromise
    }
}

let promise = new Promise((resolve, reject) => {
    resolve(100)
})

let p1 = promise.then(function (value) {
    console.log(value)
    // 这样写会发生 Promise 对象的循环调用,会抛出错误
    return p1
})

p1.then(function () {}, function (reason) {
    console.log(reason.message)     // Chaining cycle detected for promise #<Promise>
})

resolvePromise 函数增加对传递过来的两个参数进行相等判断。

function resolvePromise (newPromise, x, resolve, reject) {
    // 判断 newPromise 与 x 是否是同一个 Promise 对象
    if (newPromise === x) {
        // 触发失败回调函数
        reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
        return
    }
    ...
    ...
}

但是,当前代码尚未可以正常运行,因为上面调用 resolvePromise(newPromise, x, resolve, reject) 的时候,newPromise 还没有被赋值,此时还是 undefined,我们需要在调用 resolvePromise(newPromise, x, resolve, reject) 函数时 newPromise 是被赋值完成的。

方法就是将 newPromise 对象执行器所执行的 resolvePromise 方法变成异步任务即可,这样就可以获取到 newPromise 被赋值后的值。因为异步任务的回调函数会在所有同步任务完成后才会被执行,使用 setTimeout 改造一下即可。

// 判断状态
if (this.status === FULFILLED) {
    
    setTimeout(() => {
        // 只需要判断 x 跟 newPromise 是否相等
        // 即可判断出回调函数返回的 promise 对象是否是当前 then 方法的 Promise 对象
        let x = successCallback(this.value)
        // 将 newPromise 对象传递到 resolvePromise 函数中做判断
        resolvePromise(newPromise, x, resolve, reject)
    }, 0)
    
}

下面是增加 then 方法链式调用识别 Promise 对象自返回处理的 MyPromise 类完整代码。

const PENDING = 'pending'   // 等待
const FULFILLED = 'fulfilled'   // 成功
const REJECTED = 'rejected'     // 失败

class MyPromise {
    constructor (executor) {
        // 传入一个执行器并立即执行(执行器两个参数是函数)
        executor(this.resolve, this.reject)
    }
    
    // promise 状态初始值
    status = PENDING
    // promise 成功 resolve 传递的值(默认值)
    value = undefined
    // promise 失败 reject 传递的原因(默认值)
    reason = undefined
    
    // 成功回调
    // successCallback = undefined
    // 因为多次调用 then 方法,所以用数组来存储多个回调函数
    successCallback = []
    // 失败回调
    // failCallback = undefined
    failCallback = []
    
    resolve = value => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为成功
        this.status = FULFILLED
        // 保存 promise 成功传过来的值
        this.value = value 
        
        // 判断成功回调是否存在,如果存在,则调用
        // this.successCallback && this.successCallback(this.value)
        // 因为多次调用 then 方法,回调函数存储在数组中,所以需要循环依次调用(队列先进先出)
        while(this.successCallback.length) this.successCallback.shift()(this.value)
    }
    
    reject = reason => {
        // 如果状态不是等待,阻止程序向下执行
        // 因为 Promise 一旦状态改变,是不可以再改变的 
        if(this.status !== PENDING) return
        // 将状态更改为失败
        this.status = REJECTED
        // 保存 promise 失败传过来的原因
        this.reason = reason 
        
        // 判断失败回调是否存在,如果存在,则调用
        // this.failCallback && this.failCallback(this.reason)
        // 因为多次调用 then 方法,回调函数存储在数组中,所以需要循环依次调用(队列先进先出)
        while(this.failCallback.length) this.failCallback.shift()(this.reason)
    }
    
    // 类的所有方法都是定义在类的 prototype 属性上面的
    // 即类的方法是被定义在原型对象中的,此处 then 方法就是
    // then 方法期望的参数是回调函数
    then (successCallback, failCallback) {
    
        // 创建新的 Promise 对象作为返回值,实现链式调用 then 方法
        let newPromise = new MyPromise((resolve, reject) => {
        
            // 判断状态
            if (this.status === FULFILLED) {
                
                // 异步任务,保证获取到 newPromise 对象
                setTimeout(() => {
                    // 新的 Promise 对象的执行器调用 resolve 方法,参数是上一个 then 方法的回调函数返回值
                    // 因为是实现 then 方法的链式调用,所以只在 fulfilled 状态处理即可
                    let x = successCallback(this.value)
                    // 注释掉 resolve(x),因为返回值有两种
                    // 判断 x 的值是普通值还是 Promise 对象
                    // 如果是普通值,直接调用 resolve
                    // 如果是 Promise 对象,查看 Promise 对象返回的结果
                    // 再根据 Promise 对象返回的结果,决定调用 resolve 还是 reject
                    // 增加判断 newPromise 跟 x 是否是同一个 Promise 对象,避免程序异常
                    resolvePromise(newPromise, x, resolve, reject)
                }, 0)
                
            } else if (this.status === REJECTED) {
                failCallback(this.reason)
            } else {
            
                // pending 等待状态
                // 如果 Promsie 实例对象传入的执行器存在异步任务时
                // 会开启异步任务后立即执行 then,此时状态还是处于 pending
                // 将成功回调和失败回调函数通过类的属性储存起来
                // 以便于异步任务结束后在 resolve 或 reject 中执行 then 的回调函数
                
                // 多次调用 then 方法,回调函数存储到数组中
                // this.successCallback = successCallback
                this.successCallback.push(successCallback)
                // this.failCallback = failCallback
                this.failCallback.push(failCallback)
                
            }
            
        })
        
        return newPromise
        
    }
}

function resolvePromise (newPromise, x, resolve, reject) {
    // 判断 newPromise 与 x 是否是同一个 Promise 对象
    if (newPromise === x) {
        // 触发失败回调函数
        reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
        return
    }
    // 判断 x 是否是 Promise 实例
    if (x instanceof MyPromise) {
        // Promise 对象
        // x.then(value => resolve(value), reason => reject(reason))
        // 因为 resolve 跟 reject 都是函数,且在这里是作为 then 方法的回调函数,所以可以简化为
        x.then(resolve, reject)
    } else {
        // 普通值
        resolve(x)
    }
}

捕获错误

使用 try catch 捕获错误,并使用 reject 方法将原因传递给下一个 then 方法的失败回调函数。

  • 捕获 MyPromise 对象执行器抛出的错误
  • 捕获 then 方法成功回调抛出的错误

将 then 方法的参数变成可选参数

Promise 对象的 then 方法可以不传递参数,此时 Promise 对象的状态可以依次往后传递,直到传递给有回调函数的 then 方法。

var promise = new Promise(function (resolve, reject) {
    resolve(100)
})

promise.then().then().then(value => console.log(value))     // 100

其内部处理实现类似于:

promise.then(value => value).then(value => value).then(value => console.log(value))     // 100

我们可以在 MyPromise 类里面实现:

class MyPromise {
    ...
    ...
    
    then (successCallback, failCallback) {
        // 增加 then 参数的判断,没有参数则默认设置一个函数参数
        successCallback = successCallback ? successCallback : value => value
        failCallback = failCallback ? failCallback : reason => reason
        // 创建新的 Promise 对象作为返回值,实现链式调用 then 方法
        let newPromise = new MyPromise((resolve, reject) => {
            // 判断状态
            if (this.status === FULFILLED) {
                // 只需要判断 x 跟 newPromise 是否相等
                // 即可判断出回调函数返回的 promise 对象是否是当前 then 方法的 Promise 对象
                let x = successCallback(this.value)
                // 将 newPromise 对象传递到 resolvePromise 函数中做判断
                resolvePromise(newPromise, x, resolve, reject)
            } else if (this.status === REJECTED) {
                failCallback(this.reason)
            } else {
                // pending
                this.successCallback.push(successCallback)
                this.failCallback.push(failCallback)
            }
        })
        return newPromise
    }
}
let promise = new MyPromise((resolve, reject) => {
    resolve('成功')
})

promise.then().then().then(value => console.log(value))     // 成功
let promise = new MyPromise((resolve, reject) => {
    reject('失败')
})

promise.then().then().then(value => console.log(value), reason => console.log(reason))     // 失败

【注意】ES6 中的 Promise 对象的 then 方法期待的参数是一个回调函数,如果该参数不是函数,则会在内部被替换为 (x) => x,即原样返回 ==promise 最终结果==的函数(此现象又被称为值穿透)

Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log)   // 1

等同于

Promise.resolve(1).then(x => x).then(x => x).then(console.log(x))   // 1

Promise.all 方法的实现

Promise.all 允许按照异步代码调用的顺序执行代码。Promise.all 接收一个数组作为参数,数组里面可以填入任何值,包括普通值和 Promise 对象,这个数组中值的顺序一定是得到结果的顺序。

Promise.all 的返回值也是一个 Promise 对象,所以也可以链式调用 then 方法。

Promise.all 有一个特点,在 all 方法中所有 Promise 对象,如果状态都是成功的,那么最后 all 方法也是成功的。如果有一个失败了,那么最后 all 方法就是失败的。

Promise.all(['a', 'b', p1(), p2(), 'c']).then(result => {
    // result -> ['a', 'b', p1, p2, '3']
})

从调用方式来看,all 是一个静态方法。在 ES6class 中,使用 static 定义的方法为静态方法,该方法不能被实例对象调用,只能通过类(即构造函数)来调用,且静态方法可以与动态方法重名。静态方法中的 this 指向的是类,不是实例对象。

下面实现 Promise.all 功能:

class MyPromise {
    ...
    ...
    
    static all (array) {
        // 结果数组
        let result = []
        // 考虑到数组中 Promise 存在异步操作,故用计数器判断全部执行完成返回 Promise.all 的结果
        let index = 0
        
        return MyPromise((resolve, reject) => {
            // 添加数据函数(放在 MyPromise 里面是为了能调用 resolve)
            function addData (key, value) {
                result[key] = value
                index ++
                if (index === array.length) {
                    resolve(result)
                }
            }
            // 循环判断数组的值是普通值还是 Promise 对象
            // 如果是普通值,则直接放进结果数组
            // 如果是 Promise 对象,则先去执行 Promise 对象,再把 Promise 对象执行的结果放到结果数组中
            for (let i=0; i< array.length; i++) {
                let current = array[i]
                // instanceof 简单来说就是检测原型链的
                if (current instanceof MyPromise) {
                    // promise 对象
                    current.then(value => addData(i, value), reason => reject(reason))
                } else {
                    // 普通值
                    addData(i, current)
                }
            }
        })
    }
}

Promise.race 方法的实现

Promise.race 同样接收一个数组作为参数,数组里面可以填入任何值,包括普通值和 Promise 对象。Promise.race 的返回值也是一个 Promise 对象,所以也可以链式调用 then 方法。

Promise.race 方法的参数与 Promise.all 方法一样,如果参数的项不是 Promise 实例,会将参数转为 Promise 实例,再进一步处理。

Promise.race 的特点是,race 方法中的所有 Promise 对象,只要有一个率先改变状态,无论是成功还是失败,那就是 Promise.race 方法返回的 Promise 对象的最终结果。

下面实现 Promise.race 功能:

class MyPromise {
    ...
    ...
    
    static race (array) {
        return MyPromise((resolve, reject) => {
            // 循环判断数组的值是普通值还是 Promise 对象
            // 如果是 Promise 对象,则先去执行 Promise 对象,再把 Promise 对象执行的结果放到结果数组中
            for (let i=0; i< array.length; i++) {
                let current = array[i]
                // instanceof 简单来说就是检测原型链的
                if (current instanceof MyPromise) {
                    // promise 对象
                    current.then(value => resolve(value), reason => reject(reason))
                } else {
                    // 普通值
                    resolve(current)
                }
            }
        })
    }
}

Promise.resolve 方法的实现

resolve 方法内部,首先判断给定的参数是不是 Promise 对象。如果是 Promise 对象,则原封不动直接返回。如果不是 Promise 对象,则创建一个新的 Promise 对象,把给定的值包裹在 Promise 对象当中,然后返回这个 Promise 对象即可。

代码实现如下:

class MyPromise {
    ...
    ...
    
    static resolve (value) {
        // 如果是 Promise 对象,原封不动返回
        if (value instancof MyPromise) return value
        // 不是 Promise 对象的话,则创建并返回一个 Promise 对象
        return new MyPromise(resolve => resolve(value))
    }
}

finally 方法的实现

  • 无论当前 Promise 对象的最终状态是成功或失败,finally 方法的回调函数都会被执行
  • finally 方法后面,可以链式调用 then 方法来拿到当前这个 Promise 对象返回的最终结果
class MyPromise {
    ...
    ...
    
    finally (callback) {
        return this.then(value => {
            // callback()
            // return value
            // 保证 callback 中的异步任务的完成后再触发后面的 then 方法回调函数
            return MyPromise,resolve(callback()).then(() => value)
        }, reason => {
            // callback()
            // throw reason
            // 保证 callback 中的异步任务的完成后再触发后面的 then 方法回调函数
            return MyPromise,resolve(callback()).then(() => { throw reason })
        })
    }
}

catch 方法的实现

catch 方法是用来处理当前这个 Promise 对象最终的状态为失败的情况的,这样 then 方法里面可以不传入失败回调函的数,这个失败会被 catch 方法捕获,从而执行 catch 方法内的回调函数。

class MyPromise {
    ...
    ...
    
    catch (failCallback) {
        // 调用了 then 方法,只注册了失败回调函数
        return this.then(undefined, failCallback)
    }
}

qiankun 微前端应用实践与部署(二)

qiankun 微前端应用实践与部署(二)

下面是两种方案的简要描述。

传统部署

方式

通过配置 nginx 端口到目录的转发。

具体可查看上一篇文章

特点

需要对外开放子应用对应的端口,将编译好的应用文件放到对应的配置目录。

docker 部署

方式

首先构建主应用与子应用的 docker 镜像,通过 docker run 或者 docker-compose 的方式启动容器。

通过配置 nginx 转发规则,匹配访问路径子应用容器端口。

假设服务器 ip192.168.2.192,主应用容器端口是 8889,子应用容器端口是 71007101

其中应用容器在构建镜像时是实现了 web 服务的,容器跑起来之后在服务器上是可以通过 127.0.0.1:7100 来访问应用的。

因为前端子应用需要注册到主应用上,需要填写子应用的入口地址。

// index.js

registerMicroApps(
  [
    {
      name: 'app1',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app1',
    },
    {
      name: 'app2',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7101' : '//localhost:7101',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app2',
    }
  ]
}

此时服务器需要开放的端口是主应用的 8889,子应用的 71007101

为了减少对外开放的端口数,我们要对 8889 端口进行 nginx 路径匹配转发。

修改子应用注册信息:

// index.js

registerMicroApps(
  [
    {
      name: 'app1',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:8889/app1' : '//localhost:7100',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app1',
    },
    {
      name: 'app2',
      entry: process.env.NODE_ENV === 'production' ? '//192.168.2.192:8889/app2' : '//localhost:7101',
      container: '#subapp-viewport',
      loader,
      activeRule: '/app2',
    }
  ]
}

当前子应用在主应用配置的入口地址 entry192.168.2.192:8889/app1,实际经过 nginx 代理访问的是 127.0.0.1:7100,即实际访问的是运行在服务器的子应用。

配置 nginx 代理规则:

# nginx.conf

http {
    server {
      listen	8889;
      server_name 192.168.2.192;
      
      location /app1 {
        proxy_pass  127.0.0.1:7100

        try_files $uri $uri/ /index.html;
      }
    }
    
    server {
      listen	8889;
      server_name 192.168.2.192;
      
      location /app2 {
        proxy_pass  127.0.0.1:7101

        try_files $uri $uri/ /index.html;
      }
    }
}

主应用访问子应用流程图:

process.png

如果子应用部署在其他服务器,还需在其他服务器配置 nginx 的跨域问题

特点

访问权限规则由 nginx 的转发配置决定,可开放较少端口,对外开放的端口只有主应用服务的端口。

docker 访问外部 https 的数字证书验证问题

docker 访问外部 https 的数字证书验证问题

我们在构建 docker 镜像时一般使用的是 alpine linux 系统,默认是不带 ca-certificates 根证书的,导致无法识别外部 https 携带的数字证书。

那么,在访问的时候就会抛出 x509: certificate signed by unknown authority 的错误,导致 docker 容器的接口服务返回 500

为了解决证书验证的问题,我们要在构建 docker 镜像的时候把 ca-certificates 根证书给装上,这样就能识别来自外部 https 的数字证书了。

在编辑 Dockerfile 的时候加入以下命令即可:

RUN apk --no-cache add ca-certificates \
  && update-ca-certificates

如果不想重新构建镜像的话,可以直接进入容器:

$ docker exec -it '容器ID或容器名称' bash

然后执行安装根证书命令:

$ apk --no-cache add ca-certificates && update-ca-certificates

出现以下警告,可以忽略:

WARNING: ca-certificates.crt does not contain exactly one certificate or CRL: skipping

然后重启容器即可:

$ docker restart '容器ID或容器名称'

Flutter 读取应用资源并显示

Flutter 读取应用资源并显示

flutter 中,如果需要加载资源的话,需要在 pubspec.yaml 指定 APP 所需要的资源。

这样的话,指定的每个 Asset (资源)都会被打包在 APP 中,并且在 APP 运行时可以访问到这些资源。

最常见的 Asset 类型就是图片,指定图片资源后即可以在 APP 页面使用图片控件加载资源了。

# pubspec.yaml

flutter:
    assets:
        - assets/images/logo.png
// lib/main.dart

Image.asset('assets/images/logo.png')

使用 rootBundle 对象访问资源

APP 可以通过引入 services 包使用 rootBundle 对象来访问资源。

import 'package:flutter/services.dart';

比如访问文件 test.txttxt 文件内容是 测试文字,可以使用 rootBundle 对象的 loadString 方法。

当然,前提也是需要在 pubspec.yaml 中指定资源才能访问的到。

rootBundle.loadString('assets/txt/test.txt').then((data){
    print(data);
});

// 测试文字

因为 loadString() 返回的是 Future<String>,所以需要用 then() 接受返回的 String 类型的数据。Future 类似于 ES6 中的 Promise,当异步任务执行完成后会把结果返回给 then()

使用 FutureBuilder 控件配合加载资源

FutureBuilder 控件可以根据 Future (异步)任务的进度进行不同的处理。

FutureBuilder 有三个子属性,分别是:

  • future 异步任务获得数据的代码
  • initialData 初始化数据加载
  • builder 回调函数,处理异步处理中的快照,即异步处理的每一步状态变化都会触发回调函数

具体回调参数对象的属性可以自行网上查询。

下面是一段配合 FutureBuilder 控件实现的加载 markdown 文件并使用 markdown_widget 包进行解析显示到页面的代码。

同样是需要在 pubspec.yaml 进行资源指定,可以使用指定文件夹的形式,当前文件夹的资源都会被放进 AssetBundle

# pubspec.yaml

flutter:
    assets:
        - assets/md/

加载并显示 markdown 文件:

// 引入必要的包

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:markdown_widget/markdown_widget.dart';
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('文本组件'), centerTitle: true),
      body: Container(
        margin: EdgeInsets.all(15),
        child: FutureBuilder(
          future: rootBundle.loadString('assets/md/text.md'),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            if (snapshot.hasData) {
              // 判断已经返回数据则让 markdown 解析控件进行解析显示
              return MarkdownWidget(data: snapshot.data);
            } else {
              return Center(
                child: Text("加载中..."),
              );
            }
          },
        ),
      ),
    );
  }

效果如下:

markdown.png

JS 模拟实现 call、apply、bind

JS 模拟实现 call、apply、bind

实现call

call 的参数是直接放进去的,第二第三第 n 个参数全部都是用逗号分割

下面是 call 的用法形式:

function test(arg1, arg2) {
  console.log(arg1, arg2)
  console.log(this.a, this.b)
}

run.call(
  {
    a: "a",
    b: "b"
  },
  1,
  2
)

call 的作用是可以改变函数的上下文对象,也就是函数里 this 的指向
👉 这是我们模拟实现 call 的关键

如果一个函数作为一个对象的属性,那么通过调用这个对象的属性调用该函数,this 就是该对象

let obj = {
  a: "a",
  b: "b",
  test: function(arg1, arg2) {
    console.log(arg1, arg2)
    // this.a 就是 a; this.b 就是 b
    console.log(this.a, this.b)
  }
}

obj.test(1, 2)

下面就是模拟实现 call

Function.prototype.call2 = function(context){

    if(typeof this !== 'function'){
        throw new TypeError('Error')
    }
    
    //默认执行上下文是window
    context = context || window
    
    //this指向调用call2的函数(调用函数)
    //将"调用函数"赋值给执行上下文对象的一个属性
    //成为上下文对象的方法
    //context.fn = this
    //考虑到属性名可能重复的情况,需要一个唯一的属性名(使用Symbol)
    const fn = Symbol('fn')
    context[fn] = this
    
    //对call2传入的参数进行提取
    //[...arguments].slice(0,1)是上下文对象
    //[...arguments].slice(1)是参数数组
    const args = [...arguments].slice(1)
    //调用上下文对象的方法
    //此时方法内部的this指向的是上下文对象
    //实现重定义"调用函数"的this指向
    const result = context[fn](...args)
    
    //删除临时定义的属性
    delete context[fn]
    
    return result
    
}

// 以下是测试代码
function test(arg1, arg2) {
  console.log(arg1, arg2)       //1,2
  console.log(this.a, this.b)   //a,b
}

test.call2(
  {
    a: "a",
    b: "b"
  },
  1,
  2
)

实现apply

apply 的参数都必须放在一个数组里面传进去,即第二个参数是数组

Function.prototype.apply2 = function(context){
    
    if(typeof this !== 'function'){
        throw new TypeError('Error')
    }
    
    context = context || window
    //context.fn = this
    //考虑到属性名可能重复的情况,需要一个唯一的属性名(使用Symbol)
    const fn = Symbol('fn')
    context[fn] = this
    
    let result
    //apply要求传入的第二个参数是数组
    if(Array.isArray(arguments[1])){
        result = context[fn](...arguments[1])
    }else{
        result = context[fn]()
    }
    
    delete context[fn]
    
    return result
}

// 以下是测试代码
function test(arg1, arg2) {
  console.log(arg1, arg2)       //1,2
  console.log(this.a, this.b)   //a,b
}

test.apply2(
  {
    a: "a",
    b: "b"
  },
  [1, 2]
)

实现bind

bind 返回的是一个函数,参数传递跟 call 一样,返回的函数被调用是也可以带参数,bind 内部会将参数进行合并

因为 bind 返回的是一个函数,所以需要考虑函数作为构造函数进行 new 创建对象的情况

Function.prototype.bind2 = function(context){
    
    if(typeof this !== 'function'){
        throw new TypeError('Error')
    }
    
    context = context || window
    
    //this指向调用bind2的函数
    const that = this
    //此处的arguments指传入bind2的参数
    const args = [...arguments].slice(1)
    
    return function F(){
    
        //若返回的F函数被当作构造函数new创建实例
        //则此时this指向构造函数实例,不会改变上下文
        //若返回的F函数当作普通函数全局调用时
        //则this指向window
        if(this instanceof F){
            //此处的arguments指传入F的参数
            return new that(...args, ...arguments)
        }
        
        //return that.call(context, ...args.concat(...arguments))
        return that.apply(context, args.concat(...arguments))
        
    }
    
}

// 以下是测试代码
function test(arg1, arg2) {
  console.log(arg1, arg2)       //1,2
  console.log(this.a, this.b)   //a,b
}

const test2 = test.call2(
  {
    a: "a",
    b: "b"
  },
  1     //参数一
)

test2(2)    //参数二

Git 撤销操作

Git 撤销操作

我们在使用 git 管理代码的时候,在步骤内经常会出现一些不符合规范或错误的提交,比如在 git addgit commitgit push 操作的时候,这时候就需要做撤销相关操作的处理,再进行合乎规范以及正确的代码提交。

git 文件恢复

当我们对某个文件进行了代码修改之后,还没有进行 git add 操作,但是我们意识到不应该修改这个文件的时候,可以使用命令 git checkout -- <file> 使文件恢复到到上一次提交时的样子。

$ git checkout -- src/render/VueRender.js

git add 撤销

当已经进行了 git add 操作,想要撤销 git add 操作。

HEAD 代表当前版本,HEAD^ 代表前一个版本,HEAD^^ 代表前两个版本,以此类推。

# 全部撤销
$ git reset HEAD

git_reset_HEAD.png

# 指定文件撤销 git reset HEAD <file>,文件名可通过 git status 获取。
$ git reset HEAD src/render/VueRender.js

git commit 撤销

当已经进行了 git add,并且 git commit,但是还没有 git push

# 回退到 git add 操作
$ git reset --soft HEAD^

# 如果进行了 2 次 commit,想都撤回,可以使用以下命令,以此类推
$ git reset --soft HEAD^^

# 或者使用 HEAD~1,HEAD~2 也可以,以此类推
$ git reset --soft HEAD~1
$ git reset --soft HEAD~2

git_reset_--soft_HEAD_.png

# 不保留 `git add` 操作,则需要再进行 git add 回退,共两条命令
$ git reset --soft HEAD^
$ git reset HEAD

git commit 信息修改

不进行代码变动,git commit 操作之后,git push 操作之前,只修改 git commit 提交的信息内容。

$ git commit --amend

输入命令后,会进入 vim 编辑,修改保存即可。

git_commit_--amend.png

git push 撤销

当我们进行了 git addgit commitgit push 等操作后,我们的本地的代码已经同步到远端了。

首先我们需要在本地回退版本:

# 回退上一个版本
$ git reset --hard HEAD^

或者查看 commit 的代码版本号,使用指定的版本号进行回退:

$ git log

git_log.png

回退到版本号为 840d8a 的版本:

$ git reser --hard 840d8a

git_reset_--hard_sha1.png

如果不小心回退错了版本,没关系!
只要命令窗口没关,向上滑查看版本号,依然可以回到任意版本号。

git_reset_--hard_sha1_2.png

当我们回退到我们需要的版本后,剩下的就是同步远端的代码库:

$ git push -f

使用 -f 参数强制推送,即可覆盖远端版本库,使之与本地版本库一致。

使用 git 向开源项目提交 pr

使用 git 向开源项目提交 pr

Pull Request 是什么

⛏️ Pull Request 是一种通知机制
你修改了他人的代码,将你的修改通知原来的作者,希望他合并你的修改,这就是 Pull Request

⛏️ Pull Request 本质上是一种软件的合作方式,是将涉及不同功能的代码,纳入主干的一种流程。这个过程中,还可以进行讨论、审核和修改代码

⛏️ 简单的说是在自己本地仓库修改代码,提交到自己远程仓库,提交 PR 后被接受后,再会被合并到 master

具体流程

fork

将项目 fork 到自己的仓库中,以 vue-clicli 为例

进入到 vue-clicliGithub 项目中,点击右上角的 fork,稍等片刻,此项目便会出现在自己的仓库中

进到自己 fork 的项目中,就能看到 Clone or download 按钮,复制一下 SSH 链接或者 HTTPS 链接

通过上面的步骤,已经将远程仓库建好了

clone

将刚才 fork 过来的项目 clone 到本地,用的是你刚才复制的 SSH 链接或者 HTTPS 链接

$ git clone https://github.com/acgzone/vue-clicli.git

进到 vue-clicli 目录中,试试跑一下 git status,会提示现在是 master 分支

git remote -v 命令,可以看到此时只与自己的远程仓库建立了连接

还需要与上游建立连接,这里上游指的是一开始 fork 的那个项目源,以 vue-clicli 为例,执行如下命令:

$ git remote add upstream https://github.com/acgzone/vue-clicli.git

再用 git remote -v 可以看到

接下来就能创建分支了

创建分支

当然,一般需要提交新功能的时候才需要创建新分支,如果是修复 bug 的话,就不需要切换分支,可以直接在主分支 master 里完成修改,下面创建分支的步骤就可以省略。
继续运行命令:( 看情况是否创建新分支 )

$ git checkout -b dev

这个命令的意思是创建一个叫 dev 的分支,运行这个命令后 bash 将自动切换到新的分支下

修改代码

自行修改代码,完成开发等等

提交

可以先使用 git status 来查看有哪些文件被修改了

然后再 git add . 将要提交的文件都加上

然后再 git commit -m "modify XX",需要注意的是 git commit 只是把修改的代码提交到当前分支 ( 如果前面没有切换新分支的话,默认分支是 master ) ,"modify XX" 是本次提交的简单说明

然后再 git push origin master,这一步才是将当前分支推送到自己的远程仓库

这时,在自己的远程仓库便能看刚才 push 上去的分支了

提交 PR

找到 New pull request,需要注意的是 compare 处选择刚才提交上来的分支 ( 当前示例的是代码提交在主分支 master 的情况 )

然后点 Create pull request

写好名字,写好说明,提交

🎨 PR 创建后,就等着管理者是否接受该 PR

关于 check 不通过的问题

github 有代码自己编译和 check 机制,在你提交 pr 的时候,项目可能已经有了比较大的变更 ( 每天都有世界各地的 coderpr ),而你没有将分支保持与项目同步,所以有可能会导致 check 失败,pr 被无视

还记得我们在自己本地有一个 master 分支,然后又拉了一个 dev 分支,然后在 dev上进行修改,提交的也是 dev,然后又想起了之前有一步是"与上游建立连接",说到你可能已经知道了 master 的作用 —— 用于远程代码同步

所以每次提交 pr 前,都要先从做代码同步。过程如下:

git fetch upstream

git rebase upstream/master

git push origin master

push 完后,远程仓库便可看到你的 branch 版本和 master 分支一致了,否则会显示与 master 相差了多少次 commit

🍭 注:此处 branch 指的是你自己的远程仓库,master 指的是 fork 的项目的仓库

🌥️ 做完这些操作,就可以回到之前的步骤来操作了

JavaScript 中的 call()、apply()、bind() 的用法

JavaScript 中的 call()、apply()、bind() 的用法

例1

var name = '小陈'
var age = 18

var obj = {
  name: '小猪',
  objAge: this.age,
  fun: function(){
    console.log(this.name + '年龄' + this.age)
  }
}

obj.objAge    //18
obj.fun()    //小猪年龄undefined

例2

var name = 'PDD'
function show(){
  console.log(this.name)
}

show()    //PDD

比较一下这两者 this 的差别,第一个打印里面的 this 指向 obj,第二个全局声明的 show() 函数 thiswindow

call()、apply()、bind() 都是用来重定义 this 这个对象的!

如:

var name = '小陈'
var age = 18

var obj = {
  name: '小猪',
  objAge: this.age,
  fun: function(){
    console.log(this.name + '年龄' + this.age)
  }
}

var boy = {
  name: '朱丽叶',
  age: 16
}

obj.fun.call(boy)   //朱丽叶年龄16
obj.fun.apply(boy)    //朱丽叶年龄16
obj.fun.bind(boy)()   //朱丽叶年龄16

以上除了 bind 方法后面多了个 () 外,结果返回都一致!
由此得出结论,bind 返回的是一个新的函数,你必须调用它才会被执行。

对比call 、bind 、 apply 传参情况下

var name = '小陈'
var age = 18

var obj = {
  name: '小猪',
  objAge: this.age,
  fun: function(a, b){
    console.log(this.name + ' 年龄 ' + this.age, ' 来自 ' + a + '去往' + b)
  }
}

var boy = {
  name: '朱丽叶',
  age: 16
}

obj.fun.call(db,'广州','深圳')     //朱丽叶 年龄 16  来自 广州去往深圳
obj.fun.apply(db,['广州','深圳'])      //朱丽叶 年龄 16  来自 广州去往深圳
obj.fun.bind(db,'广州','深圳')()       //朱丽叶 年龄 16  来自 广州去往深圳
obj.fun.bind(db,['广州','深圳'])()   //朱丽叶 年龄 16  来自 广州,深圳去往undefined

从上面四个结果不难看出 call、bind、apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:

  • call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.fun.call(db,'成都', ... ,'string')

  • apply 的所有参数都必须放在一个数组里面传进去 obj.fun.apply(db,['成都', ..., 'string'])

  • bind 除了返回是函数以外,它的参数和 call 一样

Electron 踩坑记录(三)

本文内容只适用于使用 electron-vue 模板生成的 electron 工程,相关配置也是围绕其进行。当然,使用 vuecli3 生成的 electron 工程也可参考。

对于 electron-vue 工程,由于理论上默认写死的 9080 端口可能出现被占用的情况,所以应用 http 服务应该采用自我判断的方式来使得端口保证可用。

electron 引用 flash 插件打包示例

实现判断逻辑

/lib/utils/ 下创建文件 portIsOccupied.js,实现判断端口占用情况,向进程环境 process.env 注入变量 DEV_PORT,并且返回可用的端口,使得主进程页面可以通过进程环境读取可用的端口,也可以获取通过 Promise.resolve() 返回的可用端口。

const net = require('net')

function portIsOccupied(port) {

  const server = net.createServer().listen(port)
  
  return new Promise((resolve, reject) => {
  
    server.on('listening', () => {
      console.log(`the server is runnint on port ${port}`)
      server.close()
      // 使用注入进程环境变量的方式进行状态共享
      process.env.DEV_PORT = port
      process.env.PROD_PORT = port
      // 返回可用端口
      resolve(port)
    })

    server.on('error', (err) => {
      if (err.code === 'EADDRINUSE') {
        //注意这句,如占用端口号+1
        resolve(portIsOccupied(port + 1))
        console.log(`this port ${port} is occupied.try another.`)
      } else {
        reject(err)
      }
    })
    
  })

}

module.exports = portIsOccupied

配置开发模式下的端口

因为项目使用 electron-vue 生成工程,与现在主流使用的 vuecli3 生成的 electron 工程结构有差别,但逻辑基本一样。下面是针对 electron-vue 工程的配置。

找到工程的 /.electron/dev-runner.js 文件,可以看到服务是通过 WebpackDevServer 插件跑起来的,其中端口是写死的 9080

const server = new WebpackDevServer(
  compiler,
  {
    contentBase: path.join(__dirname, '../'),
    quiet: true,
    before (app, ctx) {
      app.use(hotMiddleware)
      ctx.middleware.waitUntilValid(() => {
        resolve()
      })
    }
  }
)


server.listen(9080)

稍作修改,引入上面写好的函数,即可自行判断使用空闲的端口保证应用的运行。

const portIsOccupied = require('../lib/utils/portIsOccupied')

……
……

portIsOccupied(9080).then(port => {
  server.listen(port)
})

配置生产模式下的端口

在主线程文件 /src/main/index.js 下引入判断函数,判断是生产模式则使用 express 作为本地打包文件的 http 静态服务器,使得 flash 能正常加载。

对原先写死端口的 localServer 函数进行改造,引入端口判断函数。

before :

function localServer() {
  let server = express()
  server.use(express.static(__dirname))
  server.listen(9080)
}

after :

import portIsOccupied from '../../lib/utils/portIsOccupied'

function localServer() {
  // 使用 promise 配合 await 实现同步
  return new Promise((resolve, reject) => {
    let server = express()
    server.use(express.static(__dirname))
    portIsOccupied(9080).then(port => {
      server.listen(port)
      resolve(port)
    })
  }) 
}

当在生产模式下执行 localServer 函数后,则可以在任意位置从进程环境读取到可用的端口,保证服务成功开启。

完整逻辑

下面是完整主线程文件 /src/main/index.js,具体逻辑如下:

import { app, BrowserWindow } from 'electron'
import express from 'express'

//引入自动判断端口可用函数
import portIsOccupied from '../../lib/utils/portIsOccupied'

/**
 * Set `__static` path to static files in production
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
 */
if (process.env.NODE_ENV !== 'development') {
  global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
}

//打包后的文件默认是以 "file://" 协议加载的
//因为 flash 不允许在 "file://" 协议下加载,为了解决 flash 加载的安全问题
//使用 express 用作本地服务器,使得页面运行在本地 http 端口服务
function localServer() {
  // 使用 promise 配合 await 实现同步
  return new Promise((resolve, reject) => {
    let server = express()
    server.use(express.static(__dirname));
    portIsOccupied(9080).then(port => {
      server.listen(port)
      resolve(port)
    })
  }) 
}

let mainWindow

let flashPlugins = process.arch == 'x64' 
  ? require('path').resolve(__dirname, '../../lib/pepflashplayer64_29_0_0_238.dll')
  : require('path').resolve(__dirname, '../../lib/pepflashplayer32_29_0_0_238.dll')

if (__dirname.includes(".asar")) {
  flashPlugins = process.arch == 'x64' 
    ? require('path').join(process.resourcesPath + '/lib/pepflashplayer64_29_0_0_238.dll')
    : require('path').join(process.resourcesPath + '/lib/pepflashplayer32_29_0_0_238.dll')
}
app.commandLine.appendSwitch('ppapi-flash-path', flashPlugins);
app.commandLine.appendSwitch('ppapi-flash-version', '29.0.0.238');

async function createWindow () {

  if (process.env.NODE_ENV === "production") {
    // 使用 async / await 实现同步等待,保证 process.env.PROD_PORT 的赋值
    await localServer()
  }

  const winURL = process.env.NODE_ENV === 'development'
  ? `http://localhost:${process.env.DEV_PORT}`
  // : `file://${__dirname}/index.html`
  // 解决 flash 不允许在 "file://" 协议下加载的问题
  : `http://localhost:${process.env.PROD_PORT}/index.html`

  /**
   * Initial window options
   */
  mainWindow = new BrowserWindow({
    height: 900,
    width: 1600,
    useContentSize: true,
    frame: false,
    center: true,
    fullscreenable: false, // 是否允许全屏
    center: true, // 是否出现在屏幕居中的位置
    title: 'electron-rtmp',
    backgroundColor: '#fff', // 背景色,用于transparent和frameless窗口
    titleBarStyle: 'hidden', // 标题栏的样式,有hidden、hiddenInset、customButtonsOnHover等
    resizable: false, // 是否允许拉伸大小
    'webPreferences': {
      plugins: true,
      webSecurity: false,
      defaultFontFamily: {
        standard: "Microsoft YaHei",
        defaultEncoding: "utf-8"
      }
    }
  })

  if (process.env.NODE_ENV == 'development') {
    mainWindow.webContents.openDevTools()
  }
  
  mainWindow.loadURL(winURL)

  mainWindow.on('closed', () => {
    mainWindow = null
  })

}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow()
  }
})

发布订阅模式和观察者模式

发布订阅模式和观察者模式

发布/订阅模式

  • 订阅者
  • 发布者
  • 事件中心

我们假定,存在一个“事件中心”,某个任务执行完成,就向事件中心“发布”(publish)一个事件,其他任务可以向事件中心“订阅”(subscribe)这个事件,从而知道什么时候自己可以执行。这就叫做“发布/订阅模式”(publish-subscribe pattern)

// vue 实现的**事件总线

let vm = new Vue()

// { 'dataChange': [fn1, fn2], 'handleData: [fn]' }
// 注册事件(订阅消息)
vm.$on('dataChange', () => {
    console.log('dataChange1')
})

vm.$on('dataChange', () => {
    console.log('dataChange2')
})

// 触发事件(发布消息)
vm.$emit('dataChange')

下面实现一个发布订阅:

// 手写实现发布订阅

// 事件触发器
class EventEmitter {
    constructor () { 
        // { 'click': [fn1, fn2], 'change: [fn]' }
        // 用一个对象来储存事件订阅信息,同一个事件可被订阅多次,故事件的回调函数用数组储存
        this.subs = Object.create(null)
    }
    
    // 注册事件
    $on (eventType, handler) {
        // 需要找到储存事件订阅信息对象里是否有当前事件,有则获取已有数组,否则赋值为空数组
        this.subs[eventType] = this.subs[eventType] || []   
        // 新注册事件处理函数
        this.subs[eventType].push(handler)
    }
    
    // 触发事件
    $emit (enventType) {
        // 先判断是否有当前事件,有则执行,没有则不作处理
        if(this.subs[eventType]){
            // 遍历数组调用事件函数
            this.subs[eventType].forEach(handler => {
                handler()       
            })
        }
    }
}

const em = new EventEmitter()

em.$on('click', () => {
    console.log('click1')
})
em.$on('click', () => {
    console.log('click2')
})

em.$emit('click')

观察者模式

  • 观察者(订阅者)-- Watcher
    • update(): 当事件发生时,具体要做的事情
  • 目标(发布者)-- Dep
    • subs 数组: 储存所有观察者
    • addSub(): 添加观察者
    • notify(): 当事件发生时,调用所有观察者的 update() 方法
  • 没有事件中心

关于为什么目标(发布者)用 Dep 表示,是因为 Dep 是 dependence(依赖)的缩写。因为 Watcher 观察者(订阅者)需要依赖 Dep 才能了解数据的变化,没有 Dep,Watcher 根本不可能知道数据发生了变化,当有数据变化发生时,Dep 会通知 Watcher。

// 发布者-目标
class Dep {
    constructor () {
        // 记录所有的订阅者
        this.subs = []
    }
    
    // 添加订阅者
    addSub (sub) {
        // 在添加之前,要确保订阅者存在且具有update方法
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }
    
    // 发布通知
    notify () {
        // 找到所有的订阅者并调用它们的update方法
        this.subs.forEach(sub => {
            sub.updatre()
        })
    }
}

// 订阅者-观察者
class Watcher {
    update () {
        console.log('update')
    }
}

let dep = new Dep()
let watcher = new Watcher()

dep.addSub(watcher)

dep.notify()

总结

  • 观察者模式 是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者和发布者之间是存在依赖的
  • 发布/订阅模式 由统一调度中心(事件中心)调用,因此发布者和订阅者不需要知道对方的存在

事件中心隔离了发布者和订阅者,去除它们之间的相互依赖。观察者模式中,目标与观察者是相互依赖的,而发布订阅模式中,多了个事件中心。事件中心是隔离发布者和订阅者的,减少发布者和订阅者的依赖关系,会变得更加灵活。

image.png

docker 访问宿主机的 ip 配置问题

docker 访问宿主机的 ip 配置问题

场景描述

centos7 运行 docker 容器应用时,需要连接宿主机的 mysql3306 端口,发现连接不上,docker 容器无法访问宿主机的 mysql 数据库。但是,在容器内访问外部网络是可以 ping 通的。

原因分析

centos7 上部署 docker 容器,其网络模式采用的是 bridge 模式。
启动 docker 时,docker 进程会创建一个名为 docker0 的虚拟网桥,用于宿主机与容器之间的通信。当启动一个 docker 容器时,docker 容器将会附加到虚拟网桥上,容器内的报文通过 docker0 向外转发。

如果 docker 容器访问宿主机,那么 docker0 网桥将报文直接转发到本机,报文的源地址是 docker0 网段的地址。而如果 docker 容器访问宿主机以外的机器,dockerSNAT 网桥会将报文的源地址转换为宿主机的地址,通过宿主机的网卡向外发送。

因此,当 docker 容器访问宿主机时,如果宿主机服务端口会被防火墙拦截,那么就无法连通宿主机,出现 No route to host 的错误。

而访问宿主机所在局域网内的其他机器,由于报文的源地址是宿主机 ip,因此,不会被目的机器防火墙拦截,所以可以访问。

解决问题

首先设置了 mysql 的配置文件,保证 mysql 可以被任何 ip 访问:

[mysqld]
bind-address = 0.0.0.0

修改完配置文件重启生效。
但为了安全考虑,防火墙的 3306 端口仍然是不开放外网访问的。

容器访问宿主机的地址使用 eth0 的地址,即宿主机内网 ip 地址。
运行 ipconfig 命令,查看网络的虚拟网桥相关信息。

注意:宿主机会把容器 ip 地址段当成外网 ip。(当前说明是 centos7 环境)

编辑防火墙文件 /etc/firewalld/zones/public.xml,添加下面 docker0 地址段到配置:

<rule family="ipv4">
  <source address="172.18.0.0/16"/>
  <accept/>
</rule>

重启防火墙,docker 容器即可正常访问宿主机端口。

service firewalld restart

🎨 如果有用到 docker-compose 命令,则会自动创建一个名为 br-"docker network id" 的虚拟网桥。
🎨 此时同样需要将虚拟网桥地址段配置到防火墙白名单,才能正常访问,添加配置:

<rule family="ipv4">
  <source address="172.20.0.0/16"/>
  <accept/>
</rule>

image

测试端口

在容器中测试宿主机端口是否可以连接,可以使用 wget 内网ip:端口 命令。

$ wget 172.17.25.162:3306  
wget: can not connect to remote host (172.17.25.162): Host is unreachable  #不可以连接

$ wget 172.17.25.162:3306
wget: bad header line: 5.7.29-log  #可以连接

Electron 踩坑记录(一)

Electron 踩坑记录(一)

场景描述

构建 pc 客户端,采用 electron-vue 脚手架进行快速搭建环境。
功能点在 web 端全部正常,移植代码到 electron 时出现问题的功能点有:

1. rtmp 流媒体的播放  
2. ant-design-vue UI 框架部分组件失效

🔥 播放器使用的是 vue-video-player,播放 rtmp 流需要使用 flash 技术。

electron 引用 flash 插件打包示例

问题解决

简单记录问题关键

问题:pc 客户端 ant-design-vue 部分组件不能正常工作 
原因:electron-vue 将它视为 webpack 的 externals 了,其中 UI 组件含有的 vue 文件没有被 vue-loader 正常编译,才导致功能失效
解决:找到 .electron-vue/webpack.renderer.config.js 将 ant-design-vue 加入到白名单 whiteListedModules

🔥 electron 白名单配置

问题:pc 客户端引入 flash 之后还是不能正常播放 rtmp 流
原因:electron 无法读取 vue-video-player 依赖安装的 videojs-flash 插件
解决:单独安装 videojs-flash 插件为项目的依赖 npm i videojs-flash -S
问题:编译成 pc 客户端后出现 vue-video-player 在即将 ready 这一步卡住
原因:Chromium 环境下 flash 加载的安全问题,不允许在 "file://" 协议下加载,而打包后的文件默认以 "file://" 协议加载
解决:在主线程里起一个 express 服务,使得打包后页面文件运行在本地的 http 端口服务即可

koa-jwt 实现自定义排除动态路由的鉴权

koa-jwt 实现自定义排除动态路由的鉴权

场景描述

🍭 之前在编写 PPAP.server 项目,一个基于 koa2nodejs 服务端接口程序。
由于接口采用的是 RESTful API,所以鉴权令牌由客户端携带发送到接口。
业务需求的是部分接口是需要用户登陆再进行操作,比如需要记录用户点赞的接口。
而不需要用户鉴权的接口,如查看帖子等不记录用户数据的接口。

对于路由权限控制(鉴权),项目使用的是 koa-jwt,支持对 token 的生成与校验,还能对接口路由进行过滤排除,指定不需要鉴权的接口。

如:

//配置不需要jwt验证的接口
app.use(jwtKoa({ secret: tokenUtil.secret }).unless({
  path: [
    '/user/login',
    '/user/register'
  ]
}));

这样上面两个接口 /user/login/user/register 都是可以跳过鉴权的,不需要携带 token

对于本项目来说,棘手的是项目接口大多使用了动态路由,即比如 /user/:id 这样的接口,需要用正则表达式去进行匹配。
但是动态路由 /user/:id 的请求方法可能会有 get post put delete 四种,所以不仅仅要排除配置的静态路由,还需要排除配置的特定请求方法的动态路由。

在阅读 koa-jwt 源码后,发现 koa-jwtunless 方法调用了 koa-unless 这个包,于是去阅读了 koa-unless 之后,发现可配置以下参数:

- method 它可以是一个字符串或字符串数组。如果请求方法匹配,则中间件将不会运行。
- path 它可以是字符串,正则表达式或其中任何一个的数组。如果请求路径匹配,则中间件将不会运行。
- ext 它可以是一个字符串或字符串数组。如果请求路径以这些扩展名之一结尾,则中间件将不会运行。
- custom 它必须是一个返回 true/ 的函数 false。如果函数针对给定的请求返回 true,则中间件将不会运行。该功能将通过 this 访问 Koa 的上下文
- useOriginalUrl 应该为 true 或 false,默认为 true。如果为false,path 则匹配 this.url 而不是 this.originalUrl。

结合项目的实际情况,解决方法只能是使用 custom 配置自定义函数进行判断。

解决方法

🍭 使用 custom 自定义函数进行过滤,创建文件 jwt_unless.js

/**
 * 用于判断客户端当前请求接口是否需要jwt验证
 */

//定义不需要jwt验证的接口数组(get方法)
const nonTokenApiArr = [
  '/',
  '/post'
]

//定义不需要jwt验证的接口正则数组(get方法)
const nonTokenApiRegArr = [
  /^\/user\/\d/,
  /^\/post\/\d/
]

//判断请求api是否在数组里
const isNonTokenApi = (path) => {
  return nonTokenApiArr.includes(path)
}

//判断请求api是否在正则数组里
const isNonTokenRegApi = (path) => {
  return nonTokenApiRegArr.some(p => {
    return (typeof p === 'string' && p === path) ||
      (p instanceof RegExp && !! p.exec(path))
  });
}

//判断当前请求api是否不需要jwt验证
const checkIsNonTokenApi = (ctx) => {
  if((isNonTokenApi(ctx.path) || isNonTokenRegApi(ctx.path)) && ctx.method == 'GET'){
    return true
  }else{
    //特殊post接口,不需要验证jwt
    if(ctx.path == '/user/login' || ctx.path == 'user/register'){
        return true
    }
    return false
  }
}

module.exports = {
  nonTokenApiArr,
  nonTokenApiRegArr,
  isNonTokenApi,
  isNonTokenRegApi,
  checkIsNonTokenApi
}

然后在 app.js 里引入 jwt_unless.js

const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const jwtKoa = require('koa-jwt')  // 用于路由权限控制
const app = new Koa()

const config = require('./config/config')

const tokenUtil = require('./util/token')
const router = require('./router')

const jwtUnless = require('./util/jwt_unless')  //用于判断是否需要jwt验证

//配置ctx.body解析中间件
app.use(bodyParser())

// 错误处理
app.use((ctx, next) => {
  //设置CORS跨域
  ctx.set("Access-Control-Allow-Origin", "*")
  ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE")
  ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type, Authorization")
  ctx.set("Content-Type", "application/json;charset=utf-8")
  ctx.set("Access-Control-Expose-Headers", "new_token")
  //获取token,保存全局变量
  if(ctx.request.header.authorization){
    global.token = ctx.request.header.authorization.split(' ')[1]
    //检测当前token是否到达续期时间段
    let obj = tokenUtil.parseToken()
    //解析token携带的信息
    global.uid = obj.uid
    global.name = obj.name
    global.account = obj.account
    global.email = obj.email
    global.roleId = obj.roleId
    //先解析全局变量再执行next(),保证函数实时获取到变量值
  }
  return next().then(() => {
    //执行完下面中间件后进入
    //判断不需要jwt验证的接口,跳过token续期判断
    if(jwtUnless.checkIsNonTokenApi(ctx)) return
    //判断token是否应该续期(有效时间)
    if(tokenUtil.getTokenRenewStatus()){
      //设置header
      ctx.set({
        new_token: tokenUtil.createNewToken()
      })
    }
  }).catch((err) => {
      //携带token的Authorization参数错误
      if(err.status === 401){
          ctx.status = 200
          ctx.body = {
            status: 401,
            message: '未携带token令牌或者token令牌已过期'
          }
      }else{
          throw err
      }
  })
})

//配置不需要jwt验证的接口
app.use(jwtKoa({ secret: tokenUtil.secret }).unless({
  //自定义过滤函数,详细参考koa-unless
  custom: ctx => {
    if(jwtUnless.checkIsNonTokenApi(ctx)){
      //是不需要验证token的接口
      return true
    }else{
      //是需要验证token的接口
      return false
    }
  }
}));

//初始化路由中间件
app.use(router.routes()).use(router.allowedMethods())

//监听启动窗口
app.listen(config.port, () => console.log(`PPAP.server is run on ${config.host}:${config.port}`))

到此就实现了对静态及动态路由鉴权,以及对 token 有效时间续期的判断。
以上的示例针对的是 get 方法动态路由的判断,如果限制除了 get 请求外的多个请求方法,则需要在定义正则数组的时候,将请求方法跟正则表达式对应起来,如:

const nonTokenApiRegArr = [
    { 
        path: /^\/user\/\d/,
        method: "GET"
    },
    { 
        path: /^\/user\/\d/,
        method: "POST"
    },
]
……
……
……
//判断请求api是否在正则数组里
const isNonTokenRegApi = (path, method) => {
  return nonTokenApiRegArr.some(p => {
    return (typeof p.path === 'string' && p.path === path && p.method === method) ||
      (p.path instanceof RegExp && !! p.path.exec(path) && p.method === method)
  });
}
//判断当前请求api是否不需要jwt验证
const checkIsNonTokenApi = (ctx) => {
  if(isNonTokenApi(ctx.path) || isNonTokenRegApi(ctx.path, ctx.method)){
    return true
  }else{
    return false
  }
}

更多详细请访问 https://github.com/ppap6/PPAP.server

qiankun 微前端应用实践与部署(四)

qiankun 微前端应用实践与部署(四)

一般情况下,我们对应用进行配置打包,要对图片字体等资源进行下面配置,使得资源路径正常加载。但是,在微前端模式下,子应用打包部署后,往往会出现子应用 css 文件里面引入资源路径加载失败的问题,下面就让我们来探究一下。

👉 独立应用下的 url-loader 配置:

// vue-cli 2 写法

module: {
  rules: [
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        // 此处的 utils.assetsPath 是函数,返回根据配置项拼接好的路径,如 static/fonts/[name].[hash:7].[ext] 
        name: utils.assetsPath("img/[name].[hash:7].[ext]")
      }
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        // 此处的 utils.assetsPath 是函数,返回根据配置项拼接好的路径,如 static/fonts/[name].[hash:7].[ext] 
        name: utils.assetsPath("fonts/[name].[hash:7].[ext]")
      }
    }
  ]
}

因为 url-loaderoptions 项的属性 publicPath 属性默认是 '',表示相对路径,即打包出来的资源引用 url 都会加上相对路径寻找 static 静态资源入口,比如:

/* static/css/app.e99e9aae.css */

background-header {
    background: url(../../static/img/bg_header.790a94f4.png);
}

所有应用编译打包部署后,当主应用加载子应用,子应用加载自身的 css 文件样式时,由于 其对应的 css 文件里面的图片 url 引用是相对路径,会出现子应用的资源相对路径拼接主应用 domain 的情况,即子应用的 ../../static/img/bg_header.790a94f4.png 会在主应用的 domain 下进行资源的寻找,导致加载失败 404 的情况。

解决打包后的 css 背景图片与字体图标路径问题

如果项目有用到第三方库,比如 element-ui,那么就更有必要进行处理了。因为 element-ui 的字体图标是在 css 里面引入的,还有相关背景图片的引入也是在 css 里,所以需要配置 webpackurl-loader,生产模式情况下直接指定资源前缀,使之成为绝对路径。

module: {
  rules: [
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("img/[name].[hash:7].[ext]"),
        //这里 192.168.2.192:7100 是子应用部署地址
        publicPath: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : ''
      }
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("fonts/[name].[hash:7].[ext]"),
        publicPath: process.env.NODE_ENV === 'production' ? '//192.168.2.192:7100' : ''
      }
    }
  ]
}

这样配置后,打包出来的 css 样式文件会变成:

/* static/css/app.e99e9aae.css */

background-header {
    background: url(//192.168.2.192:7100/static/img/bg_header.790a94f4.png);
}

资源是绝对路径,就不会出现上面子应用资源加载失败的情况。

但是,这种前端配置文件写死路径的做法灵活性并不好,比如不能做到编译一次,任意部署,因为路径写死,所以如果需要部署到其他服务器的话,就需要重新编译了。

接下来,讲的是实现灵活部署的方案。

结合 nginx 配置资源引用路径代理转发

我们在只编译打包一次前端应用的前提下,为了实现灵活部署,需要借助 nginx 来实现。

下面以 vue-cli 3 的配置为例,与 vue-cli 2 只是写法上的区别,其他都一样。

不过 vue-cli 3 对 webpack 配置进行了抽象,要理解配置中包含的东西会比较困难,尤其是当我们需要重写或者调整配置的时候,可以参考 审查项目的 webpack 配置

module.exports = {
  chainWebpack: (config) => 

    config.module
      .rule("fonts")
      .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/)
      .use("url-loader")
      .loader("url-loader")
      .options({
        limit: 4096,
        name: 'static/fonts/[name].[hash:8].[ext]',
        publicPath: process.env.NODE_ENV === 'production' ? '/live' : '',
      });

    config.module
      .rule("images")
      .test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
      .use("url-loader")
      .loader("url-loader")
      .options({
        limit: 4096,
        // name 代表 url-loader 会将资源写到 static/fonts/ 下
        name: 'static/img/[name].[hash:8].[ext]',
        // publicPath 代表资源引入 url 会生成 /live 的前缀,比如 /live/static/img/bg_header.790a94f4.png
        publicPath: process.env.NODE_ENV === 'production' ? '/live' : '',
      });

  }
}

假设主应用部署地址是 192.168.2.192,子应用的部署地址是 192.168.2.192:7102

打包编译部署后,子应用的 css 文件里面的图片资源引用 url 如下:

/* static/css/app.e99e9aae.css */

background-header {
    background: url(/live/static/img/bg_header.790a94f4.png);
}

主应用加载子应用的时候,子应用的资源拼接主应用 domian 后,子应用的 css 文件里面的图片资源加载路径 url 就会变成:

192.168.2.192/live/static/img/bg_header.790a94f4.png

此时的关键就是要访问到子应用的资源,而不是去主应用资源目录去找。

所以我们采用 nginx 路径代理转发端口的方案,当应用访问 192.168.2.192/live 这个路径时,经过 nginx 进行路径代理转发配置,转发到子应用端口。

配置 nginx 代理规则:

# nginx.conf

http {
  server {
    listen	80;
    server_name 192.168.2.192;
  
    location /live {
      proxy_pass  127.0.0.1:7102

      try_files $uri $uri/ /index.html;
    }
  }
}

此时真正访问的资源路径(子应用资源路径)是:

192.168.2.192:7102/static/img/bg_header.790a94f4.png

👋 至此,通过修改配置 url-loaderpublicPath 以及配置 nginx 的路径代理转发,就可以实现编译打包一次,到处部署的目的。

Vuepress 使用 CDN 优化 gh-pages 加载速度

Vuepress 使用 CDN 优化 gh-pages 加载速度

众所周知,github 在国内访问极不稳定,有时候加载速度极慢,导致国内用户体验极差。
我的 Vuepress 博客网站刚好是托管在 gh-pages 上,所以就想优化访问速度,让页面更加顺滑。

优化方案

下面优化博客加载速度的一些方案:

  • 优化打包代码文件大小
  • 压缩加载资源文件大小
  • 减少 http 请求次数
  • 采用 cdn 加速

因为 Vuepress 是静态博客,而且 Vuepress 本身会优化打包代码文件大小,所以现在方向是压缩图片等资源文件大小,并且使用 cdn 加速。

使用 CDN 加速

免费的 jsDelivr CDN 天然支持 Github 仓库的加速,那么如何使用呢?

以我的博客仓库为例,仓库地址是 https://github.com/jwchan1996/blog
其中,仓库资源可以通过 https://cdn.jsdelivr.net/gh/jwchan1996/blog + 仓库文件路径 直接访问。

比如:https://cdn.jsdelivr.net/gh/jwchan1996/blog/README.md

默认是访问 master 分支下的资源,如果需要访问其他分支的资源,需要指定分支:

# master分支
https://cdn.jsdelivr.net/gh/jwchan1996/blog@master/README.md

# gh-pages分支
https://cdn.jsdelivr.net/gh/jwchan1996/blog@gh-pages/logo.png

配置 Vuepress

下面分别是 Vuepress 编译前的博客源码与编译后的目录截图:

master 分支

master

gh-pages 分支

gh-pages

我们的目的是部署博客代码到 gh-pages 的时候使用 cdn 资源路径,而本地开发依然采用本地路径。

那么,如何配置呢?

找到 config.js 配置文件的 configureWebpack 配置:

module.exports = {
  title: '飘香豆腐的博客',
  
  
  configureWebpack: {
    
  }
}

其中 configureWebpack 是用于修改 Vuepress 内部的 Webpack 配置的,可以是一个对象,也可以是一个函数,然后返回一个对象。

因为我们需要做环境判断是开发环境还是生产环境,所以我们使用函数配置。

const path = require('path')

module.exports = {
  
  
  configureWebpack: () => {
    const NODE_ENV = process.env.NODE_ENV
    //判断是否是生产环境
    if(NODE_ENV === 'production'){
      return {
        output: {
          publicPath: 'https://cdn.jsdelivr.net/gh/jwchan1996/blog@gh-pages/'
        },
        resolve: {
          //配置路径别名
          alias: {
            'public': path.resolve(__dirname, './public') 
          }
        }
      }
    }else{
      return {
        resolve: {
          //配置路径别名
          alias: {
            'public': path.resolve(__dirname, './public') 
          }
        }
      }
    }
  }
}

此时我们 markdown 文件里面图片路径还是这样的:

![gitlab 503](/docker/docker_gitlab_restore/02.png)

这样编译出来的的 html 文件图片路径依然是 /docker/docker_gitlab_restore/02.png,因为没有识别图片为 Webpack 模块,所以没有添加任何路径前缀。

要想添加前缀,修改 markdown 文件图片地址即可,添加 Webpack 配置好的路径别名前缀:

![gitlab 503](~public/docker/docker_gitlab_restore/02.png)

这样所有 markdown 文件的图片都会被打包到 assets 目录下,如 /assets/img/02.706d49fc.png

同时 html 文件的图片路径也会加上配置的 publicPath 前缀:

# 打包后的CDN地址
https://cdn.jsdelivr.net/gh/jwchan1996/blog@gh-pages/assets/img/02.706d49fc.png
# 打包后的html文件图片标签
<img src="https://cdn.jsdelivr.net/gh/jwchan1996/blog@gh-pages/assets/img/02.706d49fc.png" alt="gitlab 503">

到此 Vuepress 的配置就完成了,将代码 pushgithub 仓库,等待自动化部署后,可以发现访问速度明显地提升了许多,顺滑许多!

具体访问体验可参考 https://jwchan.cn

centos7 使用 docker 部署 gitlab + gitlab-runner

centos7 使用 docker 部署 gitlab + gitlab-runner

快速配置应用

docker-compose.yml

使用 docker-composedocker 容器集群进行快速编排

获取 docker-gitlabdocker-compose.yml 配置文件,进行快速构建

$ wget https://raw.githubusercontent.com/sameersbn/docker-gitlab/master/docker-compose.yml

获取 docker-compose.yml 文件后,进行自定义配置。

配置环境

打开 docker-compose.yml 文件,针对 gitlab 进行环境配置

version: '2.3'

services:
  
  ...
  # 省略显示其他服务
  ...
  
  gitlab:
    restart: always
    image: sameersbn/gitlab:13.0.6
    depends_on:
    - redis
    - postgresql
    ports:
    - "10080:80"
    - "10022:22"
    volumes:
    - gitlab-data:/home/git/data:Z
    healthcheck:
      test: ["CMD", "/usr/local/sbin/healthcheck"]
      interval: 5m
      timeout: 10s
      retries: 3
      start_period: 5m
    environment:
    - DEBUG=false

    - DB_ADAPTER=postgresql
    - DB_HOST=postgresql
    - DB_PORT=5432
    - DB_USER=gitlab
    - DB_PASS=password
    - DB_NAME=gitlabhq_production

    - REDIS_HOST=redis
    - REDIS_PORT=6379

    - TZ=Asia/Kolkata
    - GITLAB_TIMEZONE=Kolkata

    - GITLAB_HTTPS=false
    - SSL_SELF_SIGNED=false

    - GITLAB_HOST=localhost
    - GITLAB_PORT=10080
    - GITLAB_SSH_PORT=10022
    - GITLAB_RELATIVE_URL_ROOT=
    - GITLAB_SECRETS_DB_KEY_BASE=long-and-random-alphanumeric-string
    - GITLAB_SECRETS_SECRET_KEY_BASE=long-and-random-alphanumeric-string
    - GITLAB_SECRETS_OTP_KEY_BASE=long-and-random-alphanumeric-string

    ...
    # 省略其他配置
    ...

参考配置文档,我们需要将时区设置为东八时区,设置数据混淆密匙,设置服务地址。

environment:
- TZ=Asia/Shanghai
- GITLAB_TIMEZONE=Asia/Shanghai

- GITLAB_HOST=192.168.2.192

设置混淆密匙,一般推荐 64 位随机字符串,可以用 pwgen 生成,可以安装 pwgen 服务,然后运行 pwgen -Bsv1 64 即可生成随机字符串。

environment:
- GITLAB_SECRETS_DB_KEY_BASE=nvqgzJdgrmr3tqsC4F9gKVNhKvTq3N7cJPjNggR93qthNhJ3MWkc7jNmNTLRXdhX
- GITLAB_SECRETS_SECRET_KEY_BASE=pcrf73fX4rM7bKxc7tcq3kwKWdtKKtrmmsHwT3J9rwCLMsK37PxCnXbMgnRpqJbk
- GITLAB_SECRETS_OTP_KEY_BASE=3d9tPCzpv7rfmVgnjN9McbztRVbp4rjxWWqFbNLTCbRz9mKkpvqqWgxMq7NM7c9w

同理,docker-compose.yml 的其他服务也需要配置东八时区。

数据卷挂载

数据卷挂载可对数据进行持久化保存,不会因为容器的删除而删除,数据挂载的目录数据会自动与容器内的数据同步,数据挂载的目录数据优先于容器内数据,即修改数据卷数据,会自动同步到容器内数据。

version: '2.3'

services:
  redis:
    restart: always
    image: redis:5.0.9
    command:
    - --loglevel warning
    volumes:
    - redis-data:/var/lib/redis:Z
    environment:
    - TZ=Asia/Shanghai

  postgresql:
    restart: always
    image: sameersbn/postgresql:11-20200524
    volumes:
    - postgresql-data:/var/lib/postgresql:Z
    environment:
    - DB_USER=gitlab
    - DB_PASS=password
    - DB_NAME=gitlabhq_production
    - DB_EXTENSION=pg_trgm
    - TZ=Asia/Shanghai

  gitlab:
    restart: always
    image: sameersbn/gitlab:13.0.6
    depends_on:
    - redis
    - postgresql
    ports:
    - "10080:80"
    - "10022:22"
    volumes:
    - gitlab-data:/home/git/data:Z
    healthcheck:
      test: ["CMD", "/usr/local/sbin/healthcheck"]
      interval: 5m
      timeout: 10s
      retries: 3
      start_period: 5m

注意:数据卷的挂载,需要在宿主机提前创建好对应的目录。

手动创建以下目录:

/app/volumes/gitlab/gitlab/
/app/volumes/gitlab/postgresql/
/app/volumes/gitlab/redis/

修改对应数据卷配置:

redis:
    restart: always
    image: redis:5.0.9
    command:
    - --loglevel warning
    volumes:
    - /app/volumes/gitlab/redis:/var/lib/redis:Z
postgresql:
    restart: always
    image: sameersbn/postgresql:11-20200524
    volumes:
    - /app/volumes/gitlab/postgresql:/var/lib/postgresql:Z
gitlab:
    restart: always
    image: sameersbn/gitlab:13.0.6
    depends_on:
    - redis
    - postgresql
    ports:
    - "10080:80"
    - "10022:22"
    volumes:
    - /app/volumes/gitlab/gitlab:/home/git/data:Z

gitlab-runner

拉下来的 docker-compose.yml 文件默认是没有 gitlab-runner 的,我们需要将 gitlab-runner 写到 docker-compose.yml 配置上来。

也要先创建数据卷挂载文件目录:

/app/volumes/gitlab-runner/config/
gitlab-runner: 
    restart: always
    image: gitlab/gitlab-runner
    depends_on:
    - gitlab
    volumes:
    - /app/volumes/gitlab-runner/config:/etc/gitlab-runner:Z
    - /var/run/docker.sock:/var/run/docker.sock
    environment:
    - TZ=Asia/Shanghai

快速构建应用

将配置好的 docker-compose.yml 文件放到 /app/docker/gitlab/ 下,执行以下命令:

$ cd /app/docker/gitlab/
$ docker-compose up

docker-compose 会自动管理 docker 容器集群,包括对镜像进行拉取、创建以及启动。

稍等片刻,我们即可通过 http://192.168.2.192:10080/ 打开 gitlab 页面,第一次打开是直接设置 root 账号的密码,设置密码后即可登录进入 gitlab 内页。

英文不好的同学可以进入个人设置那里设置 language 为简体中文。

设置语言

注册runner

什么是 runnerrunner 就是 gitlab 进行可持续集成与可持续交付过程所跑的环境容器服务。

为了进行 ci/cd => 可持续集成/可持续部署,我们需要注册 runner,一般我们注册的是共享 runner,也就是任何仓库的 ci/cd 都可以在上面跑。当然,我们也可以创建多个 runner 服务,为特定仓库指定 runner

下面以注册共享 runner 为例:

runner列表

比如

  1. 进入 runner 容器
$ docker exec -it 容器ID bash
  1. 注册 runner
$ gitlab-runner register
  1. 输入 gitlab 示例的 url
 $ Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com ):
 http://192.168.2.192:10080/
  1. 输入用来注册 runnertoken
$ Please enter the gitlab-ci token for this runner:
yrErncrc8XY_e5-g7bU8
  1. 输入 runner 的描述,随后可在 gitlab 界面中修改
$ Please enter the gitlab-ci description for this runner:
gitlab-ci
  1. 输入与 runner 绑定的标签(可修改)
$ Please enter the gitlab-ci tags for this runner (comma separated):
gitlab-ci
  1. 选择 runner 的执行方式(推荐docker
 $ Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell:
 docker

  1. 如果选择的执行方式是 docker,会要求填写默认的镜像
$ Please enter the Docker image (eg. ruby:2.1):
alpine:latest

注册成功后会在 runner 容器 ~/etc/gitlab-runner/ 目录下生成 config.toml 配置文件,这时候就可以在 gitlab 的管理页面中看到激活的 runner

激活的runner

然后,对 runner 进行修改,勾选 runner 可以选择无标签的项目(默认是同样标签的项目才能使用对应标签的 runner)。这样,runner 就可以变为共享 runner 了。

当我们需要专门为某个项目跑的 runner 时,那就不需要勾选 runner 可选择无标签选项,在下面配置添加 runner 服务的项目保存即可。

配置runner共享

可持续集成/部署

可持续集成与部署需要配置 .gitlab-ci.yml 文件,gitlab 会检查每个仓库根目录是否存在 .gitlab-ci.yml 文件,有的话 runner 则自动跑起来。

runner 会根据 .gitlab-ci.yml 文件配置,在宿主机创建容器,根据配置步骤一步步进行构建任务,构建成功或失败都会自动销毁容器。

gitlab 默认开启 auto devops 功能,如果没有 .gitlab-ci.yml 文件,则会自动运行 auto devops,如果没有配置 Auto DevOps 功能与 Kubernetes 集成的话,建议关闭默认的 auto devops 功能。

关闭默认

.gitlab-ci.yml

这是一个自动编译前端代码并发布到 gitlab page 的配置文件:

building: 
  image: node:alpine    # 指定运行环境
  stage: build          # 当前stage阶段为build
  script:               # build阶段运行的脚本
    - yarn --registry=https://registry.npm.taobao.org
    - yarn docs:build
  artifacts:            # 工件,可以缓存在gitlab的流水线记录中,供直接下载
    expire_in: 3 days   # 工件缓存的有效时间
    paths:              # 路径
      - docs/.vuepress/dist/            # 工件指向的目录,这里指整个dist目录

cache:                  # 缓存
  paths:                # 路径
    - node_modules/     # 缓存node_mudules将大大提高ci运行的速度

deploying: 
  stage: deploy         # 当前阶段为deploy
  script:               # deploy阶段运行的命令
    - rm -rf public/*   # linux命令,递归无询问删除public目录下所有文件- mv dist/* public //将dist目录下的所有文件都移动到public目录下
  artifacts:            # 工件缓存
    expire_in: 3 days   # 时效为3天
    paths:              # 路径
      - public          # 缓存整个public目录的文件
  only: 
    - master               # ceate pages下的所有操作只在 master 分支上进行

自动化

当我们提交我们的代码后,gitlab 会自动根据 .gitlab-ci.yml 的配置运行 runner

提交代码

运行通过

这样我们就实现自动化集成与部署了,大大的提高了我们的开发效率。

身份认证

我们在 gitlab 上注册了自己的账号后,为了方便身份认证,一般需要用 ssh 生成身份认证密匙,这样就不需要每次访问都要输入账号密码。

配置SHH密匙

在我们的电脑 git bash 输入:

$ ssh-keygen -t rsa -C "我们在gitlab注册的邮箱" -f ~/.ssh/gitlab_id_rsa

此时会在我们电脑用户根目录的 /.ssh 下生成私匙跟公匙:

gitlab_id_rsa
gitlab_id_rsa.pub

打开 pub 后缀的公匙,复制粘贴到 gitlab 用户设置,保存即可。

身份认证

在对 gitlab 仓库使用 git 命令的时候,如果出现提示没有权限的话,多半是因为 git 混淆了 githubgitlabssh 密钥,解决方法看下一步。

github与gitlab共存

假设我们之前就已经生成了 githubssh 密匙:

github_id_rsa
github_id_rsa.pub

在我们电脑的用户目录 /.ssh/ 下创建 config 文件,配置如下,保存即可:

#github
Host github.com
HostName github.com
IdentityFile C:/Users/jwchan/.ssh/github_id_rsa

#gitlab
Host 192.168.2.192
HostName 192.168.2.192
IdentityFile C:/Users/jwchan/.ssh/gitlab_id_rsa

这样,我们在提交代码的时候,会自动区分目标服务器从而使用对应的 ssh 密匙。

git基本操作

  1. 拉取仓库
$ git clone ssh://[email protected]:10022/jwchan/blog.git
  1. 进入仓库贡献代码
$ cd /blog/
  1. 查看仓库代码修改状态
$ git status
  1. 添加代码缓冲区
$ git add .
  1. 提交代码并注释
$ git commit -m "[fix]: bug"
  1. 推送代码到远程仓库
$ git push 

关于 axios 请求出现 OPTIONS

关于 axios 请求出现 OPTIONS

为什么 axios 先要先请求 OPTIONS

🎨 最近在用 vue 重写一个以前的 angular + thinkjs 的项目,由于项目环境的前后端分离了,就出现了跨域问题,配置了一下 CORS 解决了跨域问题之后,又出现了 axios 请求发送两次的情况。

以登录功能为例,一共发送两个请求,第一个是 OPTIONS 请求,第二个是 POST 请求。

axios 配置如下:

import axios from 'axios'
import qs from 'qs'

const baseURL = 'http://localhost:8888'

axios.interceptors.request.use((config) => {
  if(config.method  === 'post'){
    config.data = qs.stringify(config.data)
  }
  return config
},(error) =>{
  return Promise.reject(error)
})

const request = axios.create({
  baseURL,
  headers: {
    'X-Requested-With': 'XMLHttpRequest', 
    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
  },
  withCredentials: true // 允许携带cookie
})

export default request

执行功能请求的时候,控制台 XHR 请求如下:

等待options

一直处于 pending 状态,即等待中状态。

等待options

thinkjs 控制台显示请求时间如下:

options请求处理时间

post请求处理时间

👎 可以看到,OPTIONS 请求占用了差不多两分钟的时间之后,才进行 POST 请求,简直不能忍!!!

所以,问题来了,为什么会出现两次请求呢?这个 OPTIONS 请求到底是何方神圣?

CORS 是什么

CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。

CORS 的局限性

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于 IE10
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。
浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求。
实现 CORS 通信的关键是服务器,只要服务器实现了 CORS 接口,就可以跨源通信。

CORS 的类别

  • 简单请求(simple request)
  • 非简单请求(not-so-simple request)

简单请求(simple request)

只要同时满足以下两大条件,就属于简单请求:

请求方法是以下三种方法之一:
- HEAD
- GET
- POST
HTTP 的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type: 只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain

🙄 对于简单请求,浏览器直接发出 CORS 请求。
具体来说,就是在头信息之中,增加一个 Origin 字段。

  • Access-Control-Allow-Origin: 该字段是必须的。它的值要么是请求时 Origin 字段的值,要么是一个 * ,表示接受任意域名的请求。
  • Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可。
  • Access-Control-Expose-Headers: 该字段可选。CORS 请求时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到 6 个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。

非简单请求(not-so-simple request)

复杂跨域请求要满足以下:

- 请求方法不是 HEAD/GET/POST
- POST 请求的 Content-Type 并非 application/x-www-form-urlencoded、multipart/form-data、text/plain
- 请求设置了自定义的 header 字段
  1. 非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUTDELETE,或者 Content-Type 字段的类型是 application/json
  2. 非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)。
  3. "预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求来自哪个源。
  4. 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
  5. 除了 Origin 字段,"预检"请求的头信息包括两个特殊字段:
    • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。
    • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。

一旦服务器通过了"预检"请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。

解决 axios 请求 OPTIONS 的方法

😑 就是!把非简单请求转换为简单请求!!!

简单粗暴~

修改header

再次请求接口,发现 OPTIONS 请求已经没有了,直接 POST 请求响应速度杠杠的!

post请求成功

END

手写实现一个 VueRouter

手写实现一个 VueRouter

两种路由模式的实现思路

Hash 模式

  • URL# 后面的内容作为路径地址
  • 监听 hashchange 事件
  • 根据当前路由地址找到对应组件进行重新渲染

History 模式

  • 通过 window.history.pushState() 方法改变地址栏
  • 监听 popstate 事件
  • 根据当前路由地址找到对应组件重新渲染

关于 VueRouter 模拟实现的分析

image

Vue.use() 的参数支持传入一个函数或对象。如果传入函数,Vue.use() 会调用这个函数。如果传入对象,Vue.use() 内部会调用这个对象的 install 方法。

下面是 VueRouter 的类图:

image

类图一共分为三部分,上面部分是类的名称,中间部分是类的属性,下面部分是类的方法。

VueRouter 有三个属性,分别是 optionsdatarouteMap

options 属性的作用是记录构造函数中传入 的对象,传入的对象包含了 routes 属性,也就是路由规则。

routeMap 是一个对象,它是用来记录路由地址与组件的对应关系的,将来会把路由地址解析到 routeMap 中来。

data 是一个对象,它有一个属性 current,这个属性是用来记录当前路由地址的。此处设置有一个 data 对象的目的是我们需要一个响应式的对象。
路由地址发生变化之后组件要自动更新,需要调用 Vue.observable() 方法。

VueRouter 类图方法中 + 号是对外公开的方法,- 号是静态方法。其中 install 就是静态方法,用来实现 Vue 的插件机制。init 方法是用来调用后面的三个方法的。initEvent 方法用来监听 popstate 事件,用来监听浏览器历史变化。createRouteMap 方法是用来初始化 routeMap 属性的,能够把构造函数中传入的路由规则转化为键值对的形式存储到 routeMap 里面。routeMap 是一个对象,其中键就是路由地址,值是地址对应的组件,在 <router-view /> 这个组件中会使用到 routeMapinitComponents 方法是用来创建 <router-link /><router-view /> 这两个组件的。

实现 VueRouter 的 install 方法

Vue.use() 方法注册 VueRouter 的时候自动调用 VueRouterinstall
静态方法。下面模拟实现 VueRouter

install 方法中要做几件事:

  1. 判断当前插件是否已安装
  2. Vue 的构造函数记录到全局变量,因为将来需要在 VueRouter 的实例方法中使用这个 Vue 构造函数,比如在创建 <router-link /><router-view /> 这两个组件的时候需要调用 Vue.initComponents() 方法
  3. 把创建 Vue 实例时传入的 router 对象注入到所有 Vue 实例上,我们之前使用的 this.$router 就是在这时候注入到 Vue 实例上的,并且所有的组件也是 Vue 的实例
// myVueRouter.js

let _Vue = null

export default class VueRouter {
    // install 静态方法参数是 Vue 的构造函数
    static install (Vue) {
        // 1. 判断当前插件是否已经被安装
        if (VueRouter.install.installed) {
            return
        }
        VueRouter.install.installed = true
        // 2. 把 Vue 构造函数记录到全局变量
        _Vue = Vue
        // 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
        _Vue.mixin({
            beforeCreate () {
                // 判断是 Vue 根实例还是 Vue 组件,组件不需要重复注入,第一次实例化 Vue 注入即可
                if (this.$options && this.$options.router) {
                    // 只有 Vue 根实例的 $options 选项才有 router 属性
                    _Vue.prototype.$router = this.$options.router
                }
            }
        })
    }
}

实现 VueRouter 的构造函数

// myVueRouter.js

let _Vue = null

export default class VueRouter {
    ...
    ...
    constructor (options) {
        this.options = options
        this.routeMap = {}
        // observable 方法是 Vue 提供的能够将对象定义为响应式的一个方法
        this.data = _Vue.observable({
            current: '/'
        })
    }
}

实现 VueRouter 的 createRouteMap 方法

createRouteMap 方法的作用是将构造函数传入的 routes 路由规则转换为键值对的形式保存在 routeMap 对象中,其中键就是路由地址,值是地址对应的组件。将来路由地址发生变化的时候,可以根据 routeMap 中的路由地址找到对应的组件,渲染到 <router-view /> 组件中来。

// myVueRouter.js

let _Vue = null

export default class VueRouter {
    ...
    ...
    constructor (options) {
        this.options = options
        this.routeMap = {}
        // observable 方法是 Vue 提供的能够将对象定义为响应式的一个方法
        this.data = _Vue.observable({
            current: '/'
        })
    }
    
    createRouteMap () {
        // 遍历所有的路由规则,把路由规则解析成键值对的形式储存到 routeMap 中
        this.options.routes.forEach(route => {
            // 键 -> 值
            // 路由地址 -> 组件
            this.routeMap[route.path] = route.component
        })
    }
}

实现 VueRouter 的 initComponents 方法

initComponents 方法是用来创建 <router-link /><router-view /> 这两个组件的。

定义一个 init 方法来调用 createRouteMap 方法与 initComponents 方法,在 install 静态方法内 Vue 根实例初始化时调用 init 方法。

// myVueRouter.js

let _Vue = null

export default class VueRouter {
    // install 静态方法参数是 Vue 的构造函数
    static install (Vue) {
        // 1. 判断当前插件是否已经被安装
        if (VueRouter.install.installed) {
            return
        }
        VueRouter.install.installed = true
        // 2. 把 Vue 构造函数记录到全局变量
        _Vue = Vue
        // 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
        _Vue.mixin({
            beforeCreate () {
                // 判断是 Vue 根实例还是 Vue 组件,组件不需要重复注入,第一次实例化 Vue 注入即可
                if (this.$options && this.$options.router) {
                    // 只有 Vue 根实例的 $options 选项才有 router 属性
                    _Vue.prototype.$router = this.$options.router
                    // 调用定义好的 init 初始化方法 
                    this.$options.router.init()
                }
            }
        })
    }
    
    constructor (options) {
        this.options = options
        this.routeMap = {}
        // observable 方法是 Vue 提供的能够将对象定义为响应式的一个方法
        this.data = _Vue.observable({
            current: '/'
        })
    }
    
    createRouteMap () {
        // 遍历所有的路由规则,把路由规则解析成键值对的形式储存到 routeMap 中
        this.options.routes.forEach(route => {
            // 键 -> 值
            // 路由地址 -> 组件
            this.routeMap[route.path] = route.component
        })
    }
    
    // 实现这个 initComponents 方法时参数需要传 Vue 实例
    // 也可以通过全局变量 _Vue 获取,这里为了减少该方法对外部变量的依赖,使用传递 Vue 实例的方式
    initComponents (Vue) {
        Vue.component('router-link', {
            props: {
                to: String
            },
            template: '<a :href="to"><slot></slot></a>'
        })
    }
    
    // 定义一个初始化方法
    init () {
        this.createRouteMap()
        this.iniComponents(_Vue)
    }
}

如果是使用 vue-cli 创建出来的项目,默认是使用运行时版本,因为效率更高。上面自定义的 VueRouter 在使用时会报错,因为运行时 Vue 版本不支持 template

关于 Vue 的构建版本

  • 运行时版本:不支持 template 模板,需要打包的时候提前编译
  • 完整版:包含运行时和编译器,体积比运行时版本大 10K 左右,程序运行的时候模板转换成 render 函数。

使用完整版 Vue 解决 template 模板问题

脚手架创建的根目录下,配置 vue.config.jsruntimeCompiler

// vue.config.js

module.exports = {
    // 选项
    runtimeCompiler: true
}

使用运行时版本的 Vue 解决 template 问题

export default class VueRouter {
    ...
    ...
    initComponents (Vue) {
        Vue.component('router-link', {
            props: {
                to: String
            },
            //template: '<a :href="to"><slot></slot></a>'
            render (h) {
                return h ('a', {
                    attrs: {
                        href: this.to
                    }
                }, [this.$slots.default])
            }
        })
    }
}

运行时版本的 Vue 不支持 template 的编译,那就使用 render 函数进行渲染。

实现 VueRouter 的 router-view 组件

在定义 <router-view /> 组件的时候需要先找到当前路由的地址,再去 routeMap 中找到当前路由地址所对应的组件,然后借助 h 函数将组件转换成虚拟 DOM 直接返回。

export default class VueRouter {
    ...
    ...
    initComponents (Vue) {
        Vue.component('router-link', {
            props: {
                to: String
            },
            //template: '<a :href="to"><slot></slot></a>'
            render (h) {
                return h ('a', {
                    attrs: {
                        href: this.to
                    }
                }, [this.$slots.default])
            }
        })
        
        // 获取 VueRouter 实例
        const self = this
        Vue.component('router-view', {
            render (h) {
                // 获取当前路由对应的组件
                const component = self.routeMap[self.data.current]
                return h(component)
            }
        })
    }
}

然后实际使用我们自定义的 VueRouter 发现点击 <router-link /> 生成的 a 标签默认会刷新页面向服务器发生请求,我们需要给渲染生成 a 标签增加点击事件,阻止默认行为。其中 pushState 方法能够改变浏览器地址栏而不向服务器发送请求。

export default class VueRouter {
    ...
    ...
    initComponents (Vue) {
        Vue.component('router-link', {
            props: {
                to: String
            },
            //template: '<a :href="to"><slot></slot></a>'
            render (h) {
                return h ('a', {
                    attrs: {
                        href: this.to
                    },
                    on: {
                        click: this.clickHandler
                    }
                }, [this.$slots.default])
            },
            methods: {
                clickHandler (e) {
                    // 改变浏览器地址栏 url
                    window.history.pushState({}, '', this.to)
                    // 设置当前路由地址到 VueRouter 实例的响应式属性 data 中,data 的成员改变,成员所对应的组件也会自动更新
                    this.$router.data.current = this.to
                    e.preventDefault()
                }
            }
        })
        
        // 获取 VueRouter 实例
        const self = this
        Vue.component('router-view', {
            render (h) {
                // 获取当前路由对应的组件
                const component = self.routeMap[self.data.current]
                return h(component)
            }
        })
    }
}

实现 VueRouter 的 initEvent 方法

下面来实现 initEvent 方法,在这个方法中需要注册一个 popstate 事件。因为当前代码没有处理浏览器的前进后退,路由地址变了而组件没有跟着变。

注意:pushState 与 replaceState 方法都是不能触发 popstate 事件的。

export default class VueRouter {
    init () {
        this.createRouteMap()
        this.initComponents(_vue)
        this.initEvent()
    }

    initEvent () {
       window.addEventListener('popstate', () => {
           this.data.current = window.location.pathname
       })
    } 
}

处理 VueRouter 的不同路由模式

上面只是支持处理 history 模式,还需要支持 hash 模式。在 initEvent 方法内对路由模式进行判断,进行相对应的处理。还需要对 initComponents 方法进行修改,其中方法内定义的 <router-link> 组件需要处理 hash 路由模式。

export default class VueRouter {
    ...
    ...

    initComponents(Vue) {
        // 获取传入的路由模式
        const mode = this.options.mode === 'history' ? 'history' : 'hash'
        Vue.component('router-link', {
            props: {
                to: String
            },
            //template: '<a :href="to"><slot></slot></a>'
            render(h) {
                return h('a', {
                    attrs: {
                        // 增加对 hash 路由模式的处理
                        href: mode === 'history' ? this.to : `/#${this.to}`
                    },
                    on: {
                        click: this.clickHandler
                    }
                }, [this.$slots.default])
            },
            methods: {
                clickHandler(e) {
                    // hash 模式下不需要重写 a 标签默认行为,这里直接返回
                    if (mode === 'hash') return
                    // history 模式下改变浏览器地址栏 url
                    window.history.pushState({}, '', this.to)
                    // 设置当前路由地址到 VueRouter 实例的响应式属性 data 中,data 的成员改变,成员所对应的组件也会自动更新
                    this.$router.data.current = this.to
                    // history 模式下阻止 a 标签默认行为
                    e.preventDefault()
                }
            }
        })
        
        ...
        ...
    }
    
    initEvent () {
        // 对路由模式的判断以及处理,对浏览器前进后退的处理
        if (this.options.mode && this.options.mode === 'history') {
            // 监听浏览器前进后退触发的 popstate 事件,手动更改 current,触发更新组件视图
            window.addEventListener('popstate', () => {
                this.data.current = window.location.pathname
            }) 
        } else {
            // hash 模式
            // 判断是否已存在 hash 符号,不存在则添加 #/
            window.location.hash ? '' : window.location.hash = '/'
            // 第一次加载的时候对 hash 路由进行渲染
            window.addEventListener('load', () => {
                this.data.current = window.location.hash.slice(1)
            })
            // 监听 hash 变化
            window.addEventListener('hashchange', () => {
                // 这里因为是 hash 模式,所以 location.hash 的值是 #/ 开头的字符串 
                // 这里用 slice 截取去掉 #,赋值给 current,根据 routeMap 键值对触发组件的渲染
                this.data.current = window.location.hash.slice(1)
            })
        }
    }
}

完整 VueRouter 代码

下面是模拟实现的一个简单的 VueRouter 完整代码,尝试用模拟的 VueRouter 去代替 vue-cli 新创建出来的项目所引入的 vue-router,可以正常工作。

仓库代码示例

// router/index.js

// import VueRouter from 'vue-router'
import VueRouter from '../../myVueRouter'
// myVueRouter.js

let _Vue = null

export default class VueRouter {
  // install 静态方法参数是 Vue 的构造函数
  static install(Vue) {
    console.log('这是 Vue 的构造函数')
    console.log(Vue)
    // 1. 判断当前插件是否已经被安装
    if (VueRouter.install.installed) {
      return
    }
    VueRouter.install.installed = true
    // 2. 把 Vue 构造函数记录到全局变量
    _Vue = Vue
    // 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
    _Vue.mixin({
      beforeCreate() {
        // 判断是 Vue 根实例还是 Vue 组件,组件不需要重复注入,第一次实例化 Vue 注入即可
        if (this.$options && this.$options.router) {
          // 只有 Vue 根实例的 $options 选项才有 router 属性
          console.log('只有根组件 Vue 实例对象的 $options 选项有 router 属性,因为在实例化根组件 Vue 实例对象的时候才传入 router 属性')
          console.log(this)
          _Vue.prototype.$router = this.$options.router
          // 调用定义好的 init 初始化方法 
          this.$options.router.init()
        }
      }
    })
  }

  constructor(options) {
    this.options = options
    this.routeMap = {}
    // observable 方法是 Vue 提供的能够将对象定义为响应式的一个方法
    this.data = _Vue.observable({
      current: '/'
    })
  }

  init() {
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initEvent()
  }

  createRouteMap() {
    // 遍历所有的路由规则,把路由规则解析成键值对的形式储存到 routeMap 中
    this.options.routes.forEach(route => {
      // 键 -> 值
      // 路由地址 -> 组件
      this.routeMap[route.path] = route.component
    })
  }

  // 实现这个 initComponents 方法时参数需要传 Vue 实例
  // 也可以通过全局变量 _Vue 获取,这里为了减少该方法对外部变量的依赖,使用传递 Vue 实例的方式
  initComponents(Vue) {
    // 获取传入的路由模式
    const mode = this.options.mode === 'history' ? 'history' : 'hash'
    Vue.component('router-link', {
      props: {
        to: String
      },
      //template: '<a :href="to"><slot></slot></a>'
      render(h) {
        return h('a', {
          attrs: {
            // 增加对 hash 路由模式的处理
            href: mode === 'history' ? this.to : `/#${this.to}`
          },
          on: {
            click: this.clickHandler
          }
        }, [this.$slots.default])
      },
      methods: {
        clickHandler(e) {
          // hash 模式下不需要重写 a 标签默认行为,这里直接返回
          if (mode === 'hash') return
          // history 模式下改变浏览器地址栏 url
          window.history.pushState({}, '', this.to)
          // 设置当前路由地址到 VueRouter 实例的响应式属性 data 中,data 的成员改变,成员所对应的组件也会自动更新
          this.$router.data.current = this.to
          // history 模式下阻止 a 标签默认行为
          e.preventDefault()
        }
      }
    })

    // 获取 VueRouter 实例
    const self = this
    Vue.component('router-view', {
      render(h) {
        // 获取当前路由对应的组件
        const component = self.routeMap[self.data.current]
        return h(component)
      }
    })
  }

  initEvent() {
    // 对路由模式的判断以及处理,对浏览器前进后退的处理
    if (this.options.mode && this.options.mode === 'history') {
      // 监听浏览器前进后退触发的 popstate 事件,手动更改 current,触发更新组件视图
      window.addEventListener('popstate', () => {
        this.data.current = window.location.pathname
      })
    } else {
      // hash 模式
      // 判断是否已存在 hash 符号,不存在则添加 #/
      window.location.hash ? '' : window.location.hash = '/'
      // 第一次加载的时候对 hash 路由进行渲染
      window.addEventListener('load', () => {
        this.data.current = window.location.hash.slice(1)
      })
      // 监听 hash 变化
      window.addEventListener('hashchange', () => {
        // 这里因为是 hash 模式,所以 location.hash 的值是 #/ 开头的字符串 
        // 这里用 slice 截取去掉 #,赋值给 current,根据 routeMap 键值对触发组件的渲染
        this.data.current = window.location.hash.slice(1)
      })
    }
  }
}

Electron 踩坑记录(二)

Electron 踩坑记录(二)

场景描述

-- 2020-04-28 更新:由于 flash 30 版本以后会出现提示“未能正确加载必要组件”(其实是广告程序),导致失效,flash 版本应该替换为 29 版本。--

electron 引用 flash 插件打包示例

上一篇 electron 踩坑(一) 说到 electron 加载 flash 的问题
采用的是加载系统安装好的 flash 插件,需要用户提前安装好 flash 才能正常工作

app.commandLine.appendSwitch('ppapi-flash-path', app.getPath('pepperFlashSystemPlugin'));

其中 app.getPath('pepperFlashSystemPlugin') 会自动找寻系统 flash 的所在路径
但是,如果用户没装 flash 就打开应用,就会提示报错,带来不好的用户体验
所以,我们需要将 flash 嵌入应用依赖,也就是插件跟着应用打包

win 下面的软件有 32 位和 64 位的说法,而且安装位置会有不同。那么 flash 也不例外

C:\Windows\System32\Macromed\Flash\pepflashplayer64_29_0_0_238.dll
C:\Windows\SysWOW64\Macromed\Flash\pepflashplayer32_29_0_0_238.dll

当然,上面版本号会变化,但是 dll 所在路径基本是如上所示
找到 flash 所在路径后,我们就可以提取文件放到我们的应用目录下了
编译后就会成为应用安装包的一部分,这样就不需要用户手动安装 flash
🔥 那么,在 electron 目录下应该如何引入呢?

问题解决

flash 插件目录放到根目录的 lib 文件夹下

/lib/pepflashplayer64_29_0_0_238.dll
/lib/pepflashplayer32_29_0_0_238.dll

接下来需要在主程序入口文件 /src/main/index.js 下进行引入
也就是将这一句获取系统 flash 插件路径的代码

app.commandLine.appendSwitch('ppapi-flash-path', app.getPath('pepperFlashSystemPlugin'));

换为

let flashPlugins = process.arch == 'x64' 
  ? require('path').resolve(__dirname, '../../lib/pepflashplayer64_29_0_0_238.dll')
  : require('path').resolve(__dirname, '../../lib/pepflashplayer32_29_0_0_238.dll')
app.commandLine.appendSwitch('ppapi-flash-path', flashPlugins);

如果使用的是 BrowserWindow

const mainWindow = new BrowserWindow({
    height: 900,
    width: 1600,
    useContentSize: true,
    frame: false,
    center: true,
    fullscreenable: false, // 是否允许全屏
    center: true, // 是否出现在屏幕居中的位置
    title: 'Electron应用',
    backgroundColor: '#fff', // 背景色,用于transparent和frameless窗口
    titleBarStyle: 'hidden', // 标题栏的样式,有hidden、hiddenInset、customButtonsOnHover等
    resizable: false, // 是否允许拉伸大小
    webPreferences: {    //配置web参数选项  
      plugins: true,    //开启插件
      webSecurity: false,   //安全
      defaultFontFamily: {      //字体相关
        standard: "Microsoft YaHei",
        defaultEncoding: "utf-8"
      }
    }
})

其中,plugins: true 是必须要配置的,这是告诉 electron 需要使用插件
然后就是打包配置 package.json,在 build 项配置下面内容

"build": {
    ……
    ……
    "win": {
      "icon": "build/icons/icon.ico",
      "extraResources": "./lib/*.dll"    //将特定的文件排除,不打包在asar包内
    },
    ……
    ……
}

还有一个问题比较困扰的是,在第一次进行应用打包时,打包需要的三个包文件下载速度极慢,导致体验很差,需要再进行镜像配置。

"build": {
    "electronDownload": {
      "mirror": "https://npm.taobao.org/mirrors/electron/"
    },
}

至此,开发模式和生产模式下都是可以成功运行的
以下是 /src/main/index.js 完整代码

import { app, BrowserWindow } from 'electron'
import express from 'express'

if (process.env.NODE_ENV !== 'development') {
  global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
}

//打包后的文件默认是以 "file://" 协议加载的
//因为 flash 不允许在 "file://" 协议下加载,为了解决 flash 加载的安全问题
//使用 express 用作本地服务器,使得页面运行在本地 http 端口服务
function localServer() {
  let server = express();
  server.use(express.static(__dirname));
  server.listen(9080);
}

if (process.env.NODE_ENV === "production") {
  localServer();
}

let mainWindow
const winURL = process.env.NODE_ENV === 'development'
  // webpack 配置的 dev 服务端口
  ? `http://localhost:9080`
  // : `file://${__dirname}/index.html`
  // 解决 flash 不允许在 "file://" 协议下加载的问题
  : `http://localhost:9080/index.html`

let flashPlugins = process.arch == 'x64' 
  ? require('path').resolve(__dirname, '../../lib/pepflashplayer64_29_0_0_238.dll')
  : require('path').resolve(__dirname, '../../lib/pepflashplayer32_29_0_0_238.dll')

if (__dirname.includes(".asar")) {
  flashPlugins = process.arch == 'x64' 
    ? require('path').join(process.resourcesPath + '/lib/pepflashplayer64_29_0_0_238.dll')
    : require('path').join(process.resourcesPath + '/lib/pepflashplayer32_29_0_0_238.dll')
}
app.commandLine.appendSwitch('ppapi-flash-path', flashPlugins);
app.commandLine.appendSwitch('ppapi-flash-version', '29.0.0.238');

function createWindow () {
  /**
   * Initial window options
   */
  mainWindow = new BrowserWindow({
    height: 900,
    width: 1600,
    useContentSize: true,
    frame: false,
    center: true,
    fullscreenable: false, // 是否允许全屏
    center: true, // 是否出现在屏幕居中的位置
    title: 'Electron应用',
    backgroundColor: '#fff', // 背景色,用于transparent和frameless窗口
    titleBarStyle: 'hidden', // 标题栏的样式,有hidden、hiddenInset、customButtonsOnHover等
    resizable: false, // 是否允许拉伸大小
    webPreferences: {
      plugins: true,
      webSecurity: false,
      defaultFontFamily: {
        standard: "Microsoft YaHei",
        defaultEncoding: "utf-8"
      }
    }
  })

  if (process.env.NODE_ENV == 'development') {
    mainWindow.webContents.openDevTools()
  }
  
  mainWindow.loadURL(winURL)

  mainWindow.on('closed', () => {
    mainWindow = null
  })

}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow()
  }
})

End

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.