Charged and Ready
Be interested in these art
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
vuepress博客,分享记录bug的诞生
Home Page: https://jwchan.cn
License: GNU General Public License v3.0
起因是服务器 root
文件系统内存较小,只有 50G
,经常爆仓。
于是乎,就把 gitlab
整体移动到内存相对较大 home
文件系统下。
这不,转移了,我人直接裂开来。
我打开命令窗口,口嚼香糖,一顿蜻蜓点水,在键盘上滑过 cp -R /app /home
,刹那间,整整几个 G
的文件便搬了家。当然,其中包含着这个星期来所备份的文件。
然而,此时的我仍然在享受着香糖在口腔带来的愉悦,却不知下一秒,gitlab
的明天与意外哪个会先来。
我对着 gitlab
满心许下祝福,轻轻地敲下:
$ cd /home/app/docker/gitlab
$ docker-compose up
而此刻,等待我的却是,无尽的无尽。
有个 gitaly
服务启动失败!
啊,这……
在重新尝试多次启动无果后,似乎走上了一条不归路。
漆黑的命令窗口,如同深渊,凝视着你的凝视。
随着:
$ docker rm `docker ps -a | grep Exited | awk '{print $1}'`
$ docker rmi -f `docker images | grep '<none>' | awk '{print $3}'`
命令的执行,似乎变得清净起来。
清除了停止的容器与无用的镜像。干净的环境,总会为 gitlab
带来好运吧。然而,到头来还是一场梦。
依然启动不了 gitaly
服务,而带来的后果,就是仓库页面数据读取的失败。
其他页面功能正常,仓库页面访问返回 503
。
既然是转移之后出现的错误,那我再转移回来会报错吗?
随着我把命令喂给窗口后,gitlab
回到了原本属于它的家乡。正如同古诗句“乡音无改鬓毛衰”,此刻的 gitlab
与 它生长的 /app
环境却那么的格格不入。
依然是启动不了 gitaly
,那么,gitaly
启动失败跟仓库页面 503
有什么关系呢?
Gitaly 是一个 Git RPC
服务,用于处理 GitLab 进行的所有 git 调用。
后台服务,专门负责访问磁盘以高效处理 git 操作,并缓存耗时操作。所有 git 操作都通过 Gitaly 处理。
即是 gitaly
是 gitlab
使用的处理读取 git
的桥梁服务,gitaly
开不起来,那么 gitlab
的 git
仓库数据的读取便是失了效。
经过一番查找,定位问题为数据卷挂载目录下 /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
在主应用初始化会自动监听路由的变化去匹配注册好的子应用路由活动规则,同时 vue
路由也会监听路由变化。
因为主应用有自己的业务模块,需要支持页面刷新,所以采用 hash
路由模式。qiankun
官方 demo
使用的是 history
路由模式。
那么,采用 hash
路由模式的话,主应用路由会有 /#/
的前缀,比如主应用的 resource
组件路由:
http://localhost:8889/#/resource
假设 history
路由模式下子应用的注册信息为:
{
name: 'live',
entry: '//localhost:7102',
container: '#subapp-viewport',
activeRule: '/live',
}
此时 qiankun
只有命中 url
为 http://localhost:8889/live
才会加载子应用。
此处假设使用的路由切换代码为:
this.$router.push({
path: '/live'
})
所以现在切换的 url
是 http://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
,导致路由命中失败。我们期望的 url
是 http://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)哈希路由实践
前置条件: 在 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分支推送到远程库对应的远程分支上
🍍 在编写 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 节点
文章所说遇到的问题即是上述第三种情况。
微前端应用分为主应用与子应用,部署方式是分别编译好主应用与子应用,将主应用与子应用部署到 nginx
配置好的目录即可。
代码仓库 https://github.com/jwchan1996/qiankun-micro-app
分别进入 portal
、app1
、app2
根目录,执行:
开发模式
# 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()
此处 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
项目为例。
在入口文件增加 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;
}
路由需要根据 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',
}
]
)
output: {
library: 'portal',
libraryTarget: 'umd'
}
默认情况下,在 css
引用的资源使用 url-loader
加载打包出来是相对路径的,所以会出现子应用的资源拼接到主应用的 domain
的情况,造成加载资源失败。
因为 element-ui
的字体图标是在 css
里面引入的,还有相关背景图片的引入也是在 css
里,所以需要配置 webpack
的 url-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' : ''
}
}
]
}
🍭 在前后端的交互中,无可避免的存在一些需要验证身份的请求。
🍭 一般来说,使用 token
作为身份令牌可以实现身份验证的问题。
🍭 以此同时,token
作为自带描述信息的无状态数据,唯一的判断标准就是生成 token
时设置的有效期时间,当超过有效期时则作废。
🍭 我们在使用 APP
或 WEB
应用时,如果正在操作的时候,token
刚好过期了,那就出大问题了。
🍭 所有的数据请求都会失败,给用户带来极其糟糕的体验。
🍭 所以,如何才能让 token
进行续期呢?
🍤 因为 token
是无状态数据,一旦生成了,不能主动删除或者让它失效,唯一的就是等待有效期时间到。
🍤 所以,我们会想到,在 token
过期时客户端携带新的 token
来访问数据接口,是不是就可以了呢。
🍤 答案是的,那么现在需要解决的问题就是:
1.怎么返回新的 token 给到客户端
2.什么时候返回 token 使得用户登录状态得到续期
token
的值,客户端使用 axios
进行响应拦截判断是否有新 token
字段,有则保存起来。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
数据 data
更新了,但是视图没有更新
其中缘由在于对 vue
的响应式原理的理解偏差
把一个普通的 JavaScript
对象传给 Vue
实例的 data 选项,Vue
将遍历此对象所有的属性,并使用 Object.defineProperty
把这些属性全部转为 getter/setter
。
这些 getter/setter
对用户来说是不可见的,但是在内部它们让 Vue
追踪依赖,在属性被访问和修改时通知变化。
每个组件实例都有相应的 watcher
实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter
被调用时,会通知 watcher
重新计算,从而致使它关联的组件得以更新。
在 Vue
中,一般只有在 data
选项中声明的属性(或者是属性的属性)才是具有响应特性的。如果需要在 data
选项之外对已有属性添加具有响应特性的属性,需要用到 Vue
的 set
方法。
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
不能检测以下变动的数组:
vm.items[indexOfItem] = newValue
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
属性,所以该属性是不具有响应式的特性的。
但模板切切实实已经更新了,这又是怎么回事呢?
那是因为 vue
的 dom
更新是异步的,即当 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
用于说明 commit
的类别,允许使用下面 8
个标识。
feat: # 新功能(feature)
fix: # 修补 bug
docs: # 文档(documentation)
style: # 格式(不影响代码执行的变动,非css样式专用)
refactor: # 重构(既不是新增功能,也不是修改 bug 的代码变动)
test: # 增加测试
chore: # 构建过程或辅助工具的变动
revert: # 对之前修改代码 commit 记录的还原
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
是 commit
目的的简短描述,不超过 50
个字符
$ git commit -m "feat(course): 完成课程管理模块"
github
还提供了自动识别 issue
与 pr
链接的功能。
在提交信息后面加上对应的 issue_id
或 pr_id
,能够自动识别为对应链接,点击可跳转到对应界面。
$ git commit -m "fix(user): check user pwd (#1)"
这样的话,会自动识别 #1
为链接,跳到对应的 issue
或者 pr
页面。
为了达到提交信息规范的校验效果,我们使用配置本地 git hooks
模板的方法,只需设置一次,之后从github
上 clone
下来的代码都会沿用当前 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 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 元素的大小。
<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:边框宽度
<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 的 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>
<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 属性指定了矩形的宽度和高度(单位像素)。
<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 属性,指定了椭圆横向轴和纵向轴的半径(单位像素)。
<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 属性指定了每个端点的坐标,横坐标与纵坐标之间与逗号分隔,点与点之间用空格分隔。
<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:闭合路径
<svg width="300" height="40">
<text x="20" y="25">Hello World</text>
</svg>
text 的 x 属性和 y 属性,表示文本区块基线(baseline)起点的横坐标和纵坐标。文字的样式可以用 class 或 style 属性指定。
<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 标签用于将多个形状组成一个组(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 标签用于自定义形状,它内部的代码不会显示,仅供引用
<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 标签用于自定义一个形状,该形状可以被引用来平铺一个区域。
<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 的宽度和长度是实际的像素值。然后,指定这个模式去填充下面的矩形。
<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 属性表示图像的来源。
<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>
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)旋转。
如果 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 属性。
使用 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 文件就是一段 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 元素字符串。
下面是实现一个 Promise
的一些关键点:
Promise
是一个类,在执行这个类的时候,需要传递一个执行器(executor
)进去,执行器会立即执行。Promise
中有三种状态,分别为成功(fulfilled
)、失败(rejected
)和等待(pending
)。其中 pending
状态会变为 fulfilled
或者 rejected
,且一旦状态确定就不可以更改。resolve
和 reject
函数是用来改变状态的: resolve -> fulfilled
、reject -> 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)
})
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)
}
}
}
假设在我们在 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
类中添加两个新属性 successCallback
和 failCallback
来接收 then
方法的成功回调函数和失败回调函数,以便于在异步任务结束后能在 resolve
或 reject
属性方法内进行调用,使 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
}
}
}
同一个 Promise
实例对象的 then
方法是可以被多次调用的,当 Promise
状态变为成功或失败时,then
方法里面的成功或失败回调函数是要被依次调用的。其中,Promise
实例对象传入的执行器里面的代码有可能是同步执行也有可能是异步执行。
如果是同步执行,执行 then
方法的时候 Promise
状态已经改变,那么执行 then
方法内相对应的回调函数即可。如果是异步执行,那么我们需要将多次调用 then
方法所触发的回调函数用数组储存起来,等待 Promise
执行器的异步代码执行完毕后,根据返回状态成功或失败,在 resolve
或 reject
属性方法内循环数组,依次调用(队列先进先出)回调函数即可。
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)
}
}
}
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
方法回调函数里,不能返回当前 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
方法成功回调抛出的错误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
对象,这个数组中值的顺序一定是得到结果的顺序。
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
是一个静态方法。在 ES6
的 class
中,使用 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
对象。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)
}
}
})
}
}
在 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))
}
}
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
方法是用来处理当前这个 Promise
对象最终的状态为失败的情况的,这样 then
方法里面可以不传入失败回调函的数,这个失败会被 catch
方法捕获,从而执行 catch
方法内的回调函数。
class MyPromise {
...
...
catch (failCallback) {
// 调用了 then 方法,只注册了失败回调函数
return this.then(undefined, failCallback)
}
}
下面是两种方案的简要描述。
通过配置 nginx
端口到目录的转发。
具体可查看上一篇文章
需要对外开放子应用对应的端口,将编译好的应用文件放到对应的配置目录。
首先构建主应用与子应用的 docker
镜像,通过 docker run
或者 docker-compose
的方式启动容器。
通过配置 nginx
转发规则,匹配访问路径子应用容器端口。
假设服务器 ip
是 192.168.2.192
,主应用容器端口是 8889
,子应用容器端口是 7100
、7101
。
其中应用容器在构建镜像时是实现了 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
,子应用的 7100
、7101
。
为了减少对外开放的端口数,我们要对 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',
}
]
}
当前子应用在主应用配置的入口地址 entry
是 192.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;
}
}
}
主应用访问子应用流程图:
如果子应用部署在其他服务器,还需在其他服务器配置
nginx
的跨域问题
访问权限规则由 nginx
的转发配置决定,可开放较少端口,对外开放的端口只有主应用服务的端口。
我们在构建 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
中,如果需要加载资源的话,需要在 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')
APP
可以通过引入 services
包使用 rootBundle
对象来访问资源。
import 'package:flutter/services.dart';
比如访问文件 test.txt
,txt
文件内容是 测试文字
,可以使用 rootBundle
对象的 loadString
方法。
当然,前提也是需要在 pubspec.yaml
中指定资源才能访问的到。
rootBundle.loadString('assets/txt/test.txt').then((data){
print(data);
});
// 测试文字
因为 loadString()
返回的是 Future<String>
,所以需要用 then()
接受返回的 String
类型的数据。Future
类似于 ES6
中的 Promise
,当异步任务执行完成后会把结果返回给 then()
。
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("加载中..."),
);
}
},
),
),
);
}
效果如下:
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
的参数都必须放在一个数组里面传进去,即第二个参数是数组
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
返回的是一个函数,参数传递跟 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 add
,git commit
,git push
操作的时候,这时候就需要做撤销相关操作的处理,再进行合乎规范以及正确的代码提交。
当我们对某个文件进行了代码修改之后,还没有进行 git add
操作,但是我们意识到不应该修改这个文件的时候,可以使用命令 git checkout -- <file>
使文件恢复到到上一次提交时的样子。
$ git checkout -- src/render/VueRender.js
当已经进行了 git add
操作,想要撤销 git add
操作。
HEAD
代表当前版本,HEAD^
代表前一个版本,HEAD^^
代表前两个版本,以此类推。
# 全部撤销
$ git reset HEAD
# 指定文件撤销 git reset HEAD <file>,文件名可通过 git status 获取。
$ git reset HEAD src/render/VueRender.js
当已经进行了 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 add` 操作,则需要再进行 git add 回退,共两条命令
$ git reset --soft HEAD^
$ git reset HEAD
不进行代码变动,git commit
操作之后,git push
操作之前,只修改 git commit
提交的信息内容。
$ git commit --amend
输入命令后,会进入 vim
编辑,修改保存即可。
当我们进行了 git add
,git commit
,git push
等操作后,我们的本地的代码已经同步到远端了。
首先我们需要在本地回退版本:
# 回退上一个版本
$ git reset --hard HEAD^
或者查看 commit
的代码版本号,使用指定的版本号进行回退:
$ git log
回退到版本号为 840d8a
的版本:
$ git reser --hard 840d8a
如果不小心回退错了版本,没关系!
只要命令窗口没关,向上滑查看版本号,依然可以回到任意版本号。
当我们回退到我们需要的版本后,剩下的就是同步远端的代码库:
$ git push -f
使用 -f
参数强制推送,即可覆盖远端版本库,使之与本地版本库一致。
⛏️ Pull Request
是一种通知机制
你修改了他人的代码,将你的修改通知原来的作者,希望他合并你的修改,这就是 Pull Request
⛏️ Pull Request
本质上是一种软件的合作方式,是将涉及不同功能的代码,纳入主干的一种流程。这个过程中,还可以进行讨论、审核和修改代码
⛏️ 简单的说是在自己本地仓库修改代码,提交到自己远程仓库,提交 PR
后被接受后,再会被合并到 master
将项目 fork
到自己的仓库中,以 vue-clicli
为例
进入到 vue-clicli
的 Github
项目中,点击右上角的 fork
,稍等片刻,此项目便会出现在自己的仓库中
进到自己 fork
的项目中,就能看到 Clone or download
按钮,复制一下 SSH
链接或者 HTTPS
链接
通过上面的步骤,已经将远程仓库建好了
将刚才 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
上去的分支了
找到 New pull request
,需要注意的是 compare
处选择刚才提交上来的分支 ( 当前示例的是代码提交在主分支 master
的情况 )
然后点 Create pull request
写好名字,写好说明,提交
🎨 PR
创建后,就等着管理者是否接受该 PR
了
github
有代码自己编译和 check
机制,在你提交 pr
的时候,项目可能已经有了比较大的变更 ( 每天都有世界各地的 coder
提 pr
),而你没有将分支保持与项目同步,所以有可能会导致 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
的项目的仓库
🌥️ 做完这些操作,就可以回到之前的步骤来操作了
例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()
函数 this
是 window
。
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-vue
模板生成的 electron
工程,相关配置也是围绕其进行。当然,使用 vuecli3
生成的 electron
工程也可参考。
对于 electron-vue
工程,由于理论上默认写死的 9080
端口可能出现被占用的情况,所以应用 http
服务应该采用自我判断的方式来使得端口保证可用。
在 /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')
关于为什么目标(发布者)用 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()
事件中心隔离了发布者和订阅者,去除它们之间的相互依赖。观察者模式中,目标与观察者是相互依赖的,而发布订阅模式中,多了个事件中心。事件中心是隔离发布者和订阅者的,减少发布者和订阅者的依赖关系,会变得更加灵活。
在 centos7
运行 docker
容器应用时,需要连接宿主机的 mysql
的 3306
端口,发现连接不上,docker
容器无法访问宿主机的 mysql
数据库。但是,在容器内访问外部网络是可以 ping
通的。
在 centos7
上部署 docker
容器,其网络模式采用的是 bridge
模式。
启动 docker
时,docker
进程会创建一个名为 docker0
的虚拟网桥,用于宿主机与容器之间的通信。当启动一个 docker
容器时,docker
容器将会附加到虚拟网桥上,容器内的报文通过 docker0
向外转发。
如果 docker
容器访问宿主机,那么 docker0
网桥将报文直接转发到本机,报文的源地址是 docker0
网段的地址。而如果 docker
容器访问宿主机以外的机器,docker
的 SNAT
网桥会将报文的源地址转换为宿主机的地址,通过宿主机的网卡向外发送。
因此,当 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>
在容器中测试宿主机端口是否可以连接,可以使用 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 #可以连接
构建 pc
客户端,采用 electron-vue
脚手架进行快速搭建环境。
功能点在 web
端全部正常,移植代码到 electron
时出现问题的功能点有:
1. rtmp 流媒体的播放
2. ant-design-vue UI 框架部分组件失效
🔥 播放器使用的是 vue-video-player
,播放 rtmp
流需要使用 flash
技术。
简单记录问题关键
问题:pc 客户端 ant-design-vue 部分组件不能正常工作
原因:electron-vue 将它视为 webpack 的 externals 了,其中 UI 组件含有的 vue 文件没有被 vue-loader 正常编译,才导致功能失效
解决:找到 .electron-vue/webpack.renderer.config.js 将 ant-design-vue 加入到白名单 whiteListedModules
问题: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 端口服务即可
🍭 之前在编写 PPAP.server
项目,一个基于 koa2
的 nodejs
服务端接口程序。
由于接口采用的是 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-jwt
的 unless
方法调用了 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
}
}
一般情况下,我们对应用进行配置打包,要对图片字体等资源进行下面配置,使得资源路径正常加载。但是,在微前端模式下,子应用打包部署后,往往会出现子应用 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-loader
的 options
项的属性 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
的情况。
如果项目有用到第三方库,比如 element-ui
,那么就更有必要进行处理了。因为 element-ui
的字体图标是在 css
里面引入的,还有相关背景图片的引入也是在 css
里,所以需要配置 webpack
的 url-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
来实现。
下面以 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-loader
的 publicPath
以及配置 nginx
的路径代理转发,就可以实现编译打包一次,到处部署的目的。
众所周知,github
在国内访问极不稳定,有时候加载速度极慢,导致国内用户体验极差。
我的 Vuepress
博客网站刚好是托管在 gh-pages
上,所以就想优化访问速度,让页面更加顺滑。
下面优化博客加载速度的一些方案:
http
请求次数cdn
加速因为 Vuepress
是静态博客,而且 Vuepress
本身会优化打包代码文件大小,所以现在方向是压缩图片等资源文件大小,并且使用 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
编译前的博客源码与编译后的目录截图:
master 分支
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
的配置就完成了,将代码 push
到 github
仓库,等待自动化部署后,可以发现访问速度明显地提升了许多,顺滑许多!
具体访问体验可参考 https://jwchan.cn
使用 docker-compose
对 docker
容器集群进行快速编排
获取 docker-gitlab
的 docker-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
拉下来的 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
,runner
就是 gitlab
进行可持续集成与可持续交付过程所跑的环境容器服务。
为了进行 ci/cd
=> 可持续集成/可持续部署,我们需要注册 runner
,一般我们注册的是共享 runner
,也就是任何仓库的 ci/cd
都可以在上面跑。当然,我们也可以创建多个 runner
服务,为特定仓库指定 runner
。
下面以注册共享 runner
为例:
比如
runner
容器$ docker exec -it 容器ID bash
runner
$ gitlab-runner register
gitlab
示例的 url
$ Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com ):
http://192.168.2.192:10080/
runner
的 token
$ Please enter the gitlab-ci token for this runner:
yrErncrc8XY_e5-g7bU8
runner
的描述,随后可在 gitlab
界面中修改$ Please enter the gitlab-ci description for this runner:
gitlab-ci
runner
绑定的标签(可修改)$ Please enter the gitlab-ci tags for this runner (comma separated):
gitlab-ci
runner
的执行方式(推荐docker
) $ Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell:
docker
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
服务的项目保存即可。
可持续集成与部署需要配置 .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 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
生成身份认证密匙,这样就不需要每次访问都要输入账号密码。
在我们的电脑 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
混淆了 github
与 gitlab
的 ssh
密钥,解决方法看下一步。
假设我们之前就已经生成了 github
的 ssh
密匙:
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 clone ssh://[email protected]:10022/jwchan/blog.git
$ cd /blog/
$ git status
$ git add .
$ git commit -m "[fix]: bug"
$ git push
🎨 最近在用 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
请求如下:
一直处于 pending
状态,即等待中状态。
thinkjs
控制台显示请求时间如下:
👎 可以看到,OPTIONS
请求占用了差不多两分钟的时间之后,才进行 POST
请求,简直不能忍!!!
所以,问题来了,为什么会出现两次请求呢?这个 OPTIONS
请求到底是何方神圣?
CORS
是一个 W3C
标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出 XMLHttpRequest
请求,从而克服了 AJAX
只能同源使用的限制。
CORS
需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于 IE10
。
整个 CORS
通信过程,都是浏览器自动完成,不需要用户参与。
浏览器一旦发现 AJAX
请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求。
实现 CORS
通信的关键是服务器,只要服务器实现了 CORS
接口,就可以跨源通信。
只要同时满足以下两大条件,就属于简单请求:
请求方法是以下三种方法之一:
- 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-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers
里面指定。复杂跨域请求要满足以下:
- 请求方法不是 HEAD/GET/POST
- POST 请求的 Content-Type 并非 application/x-www-form-urlencoded、multipart/form-data、text/plain
- 请求设置了自定义的 header 字段
PUT
或 DELETE
,或者 Content-Type
字段的类型是 application/json
。CORS
请求,会在正式通信之前,增加一次 HTTP
查询请求,称为"预检"请求(preflight)。OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字段是 Origin
,表示请求来自哪个源。HTTP
动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest
请求,否则就报错。Origin
字段,"预检"请求的头信息包括两个特殊字段:
Access-Control-Request-Method
:该字段是必须的,用来列出浏览器的 CORS
请求会用到哪些 HTTP
方法。Access-Control-Request-Headers
:该字段是一个逗号分隔的字符串,指定浏览器 CORS
请求会额外发送的头信息字段。一旦服务器通过了"预检"请求,以后每次浏览器正常的 CORS
请求,就都跟简单请求一样,会有一个 Origin
头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin
头信息字段。
😑 就是!把非简单请求转换为简单请求!!!
简单粗暴~
再次请求接口,发现 OPTIONS
请求已经没有了,直接 POST
请求响应速度杠杠的!
END
URL
中 #
后面的内容作为路径地址hashchange
事件window.history.pushState()
方法改变地址栏popstate
事件Vue.use()
的参数支持传入一个函数或对象。如果传入函数,Vue.use()
会调用这个函数。如果传入对象,Vue.use()
内部会调用这个对象的 install
方法。
下面是 VueRouter
的类图:
类图一共分为三部分,上面部分是类的名称,中间部分是类的属性,下面部分是类的方法。
VueRouter
有三个属性,分别是 options
、data
和 routeMap
。
options
属性的作用是记录构造函数中传入 的对象,传入的对象包含了 routes
属性,也就是路由规则。
routeMap
是一个对象,它是用来记录路由地址与组件的对应关系的,将来会把路由地址解析到 routeMap
中来。
data
是一个对象,它有一个属性 current
,这个属性是用来记录当前路由地址的。此处设置有一个 data
对象的目的是我们需要一个响应式的对象。
路由地址发生变化之后组件要自动更新,需要调用 Vue.observable()
方法。
VueRouter
类图方法中 +
号是对外公开的方法,-
号是静态方法。其中 install
就是静态方法,用来实现 Vue
的插件机制。init
方法是用来调用后面的三个方法的。initEvent
方法用来监听 popstate
事件,用来监听浏览器历史变化。createRouteMap
方法是用来初始化 routeMap
属性的,能够把构造函数中传入的路由规则转化为键值对的形式存储到 routeMap
里面。routeMap
是一个对象,其中键就是路由地址,值是地址对应的组件,在 <router-view />
这个组件中会使用到 routeMap
。initComponents
方法是用来创建 <router-link />
和 <router-view />
这两个组件的。
Vue.use()
方法注册 VueRouter
的时候自动调用 VueRouter
的 install
静态方法。下面模拟实现 VueRouter
。
install
方法中要做几件事:
Vue
的构造函数记录到全局变量,因为将来需要在 VueRouter
的实例方法中使用这个 Vue
构造函数,比如在创建 <router-link />
和 <router-view />
这两个组件的时候需要调用 Vue.initComponents()
方法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
}
}
})
}
}
// myVueRouter.js
let _Vue = null
export default class VueRouter {
...
...
constructor (options) {
this.options = options
this.routeMap = {}
// observable 方法是 Vue 提供的能够将对象定义为响应式的一个方法
this.data = _Vue.observable({
current: '/'
})
}
}
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
})
}
}
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
。
template
模板,需要打包的时候提前编译10K
左右,程序运行的时候模板转换成 render
函数。脚手架创建的根目录下,配置 vue.config.js
的 runtimeCompiler
。
// vue.config.js
module.exports = {
// 选项
runtimeCompiler: true
}
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
函数进行渲染。
在定义 <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)
}
})
}
}
下面来实现 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
})
}
}
上面只是支持处理 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
去代替 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)
})
}
}
}
-- 2020-04-28
更新:由于 flash 30
版本以后会出现提示“未能正确加载必要组件”(其实是广告程序),导致失效,flash
版本应该替换为 29
版本。--
上一篇 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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.