My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

一些奇思妙想(一)

发表于 2020-08-02

为什么typeof null –>object?

解析器在存值的时候会以二进制形式存入,
编译时,如果一个值的二进制的前三位均为0的话,
那用typeof检测该值时,拿到的就是object
在null转二进制时,所有位都是0,所以typeof null —->object

为什么NaN != NaN —>true

NaN这个东西在计算机数值里面,是个浮点数,小数部分的位连续多位是不确定的,即在一定范围内的这个浮点数在获取的时候我们都可以叫它NaN

V8引擎调用c++的类库limit 使用它的numeric_limit的quiet_NaN()产生NaN时,
明显生成的是浮点数据类型,即每次生成的NaN是不同的数值
所以不相等

为什么js的对象的key值不能是对象

当试图用非字符串类型做对象key值时,其值会被toString方法静默转为字符串类,
当非字符串类型为对象时,如果作为key的两个对象属于一个类的实例,
那么他们转化成字符串的结果将会是相同的,赋值时,就会导致后续赋值覆盖前者的值

My Little World

一些问题小结

发表于 2020-08-02

响应式两种方案

前后端通过判断userAgent来跳转不同站点

使用media query媒体查询让页面根据不同设备自动改变页面布局和显示

移动端端适配方案

zoom
vm/vh
rem (font-size Media Query)
flex
viewport(scale=1/dpr)
链接

1
2
3
4
5
6
7
屏幕尺寸、屏幕分辨率-->对角线分辨率/屏幕尺寸-->屏幕像素密度PPI
|
设备像素比dpr = 物理像素 / 设备独立像素dip(dp)
|
viewport: scale
|
CSS像素px

实现动画的6种方式

纯JS(利用setInterval,16ms)
SVG (利用标签进行配置)
canvas(利用js API进行配置)
css3-transition(属于过度动画,不能独立实现动画)
css3-animation(纯css实现动画)
requestAnimationFrame(类似setInteval,但性能消耗低)

前两种兼容性良好
后四种适合移动端开发

处理浏览器css样式兼容

reset:清除浏览器默认样式,针对具体元素标签重写样式通过清除保持一致
normalize:样式规范确定的情况下,直接重写样式,通过使用默认统一保持一致
neat:前两者结合针对确定的样式直接重写,不确定的清除

css 样式权重
!important
内联(1000)
id(100)
类(10)
元素(1)

根据浏览器设备屏幕宽度和分辨率加载不同大小的图片

使用Media Query 判断加载不同背景图
使用H5的Picture标签
使用模板语言进行判断,渲染不同图片
请求时带上相关参数,让服务器吐出不同大小图片

JS 可能出现内存泄露的常见场景

闭包函数
全局变量
对象属性循环引用
DOM结点删除时未解绑事件
Map和Set的属性直接删除

其他

1.
数据类型在内存中分布
常量池:boolean,number,string
栈:undefined
堆:object
函数定义区:function(其实在堆里面)
=== 比较变量地址;==会通过V8进行隐式转换

2.
把Object 看作普通类,Object 是Function的实例
Object.proto === Function.prototype
把Object 看作构造函数,functionxxx 是 Object的派生
functionxxx.proto === Function.prototype
Function.prototype.proto === Object.prototype
Object.proto.proto === Object.prototype

  1. CommonJs AMD CMD NodeModules

4.LESS/SASS存在价值,有什么作用
编程式CSS,可维护性,可扩展性,静态样式一定程度上可以实现动态化

5.
前端基础:HTML/CSS/ECMAScript jQuery/bootStrap
新前端:React/VUE/Angular CommonJS/AMD/CMD/NodeModule
工具:npm/bowser 预编译(LESS\SASS\Babel)构建工具(webpack\gulp\grunt)
应用端服务器:Node Web应用\http协议\RPC\发布部署\微服务
跨平台开发:PC(window,mac)前端(web+h5)移动端(andorid,ios)(flutter\react-native)\小程序

My Little World

动态添加路由

发表于 2020-07-18

背景

需要将动态路由添加到当前静态路由的子路由中
即在路由的children属性中添加新路由

原理

使用router.addRoutes方法API进行添加

解决办法

在beforeEach方法中请求动态路由进行添加,
通过设置flag,保证动态路由仅请求一次

问题

直接增加同名路由无法覆盖

1
2
3
4
5
6
7
8
9
10
const routes = [
{ path: '/foo',name:'foo', component: Foo,children:[{ path: '/bar2/:id?',name:'bar2', component: Foo1 }]}
]
const routes1 = [
{ path: '/foo',name:'foo', component: Foo ,children:[{ path: '/bar21/:id?',name:'bar21', component: Foo2 }]},
]
const router = new VueRouter({
routes
})
router.addRoutes(routes1)

添加路由记录时,如果之前已经添加过同名路由,则只会警告,不会更新
所以如果想通过传入不同的children进行子路由更新无法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap);
}

routes.forEach(function (route) {
addRouteRecord(pathList, pathMap, nameMap, route);
});

function addRouteRecord ( pathList, pathMap, nameMap ) {
//如果有子路由,递归添加到路由表中
if (route.children) {
route.children.forEach(function (child) {
var childMatchAs = matchAs
? cleanPath((matchAs + "/" + (child.path)))
: undefined;
console.log(childMatchAs)
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
});
}

if (!pathMap[record.path]) { 关键!!!!!!详见下文
pathList.push(record.path);
pathMap[record.path] = record;
}

if (name) {
if (!nameMap[name]) {
nameMap[name] = record;
} else if ( !matchAs) { //直接警告,不更新
warn(
false,
"Duplicate named routes definition: " +
"{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
);
}
}
}

addRoutes时,子路由作为children属性,依托父路由添加时,子路由被递归添加后,
但父路由因为之前已经添加过,所以不会再对父路由进行任何处理,新添加的子路由
不会跟随父路由更新到路由表中,即addRoutes过程不会对原来老路由产生任何变动
所以新的子路由也不会被添加进去

解决办法一

拼接好路由后,通过重新生成matcher对象,清空原始路由信息,再将最终路由添加进去
详见
缺点:容易引发各种问题,且引起重复路由

解决办法二

按照API规则添加路由

1
2
router.addRoutes(routes: Array<RouteConfig>)
动态添加更多的路由规则。参数必须是一个符合 routes 选项要求的数组。

将待更新的路由对象抽离,拿到动态路由拼接好后,与404路由组成数组,直接添加路由
详见

无法正常刷新

F12刷新浏览器页面后路由对象name为null,无法正常跳转
如果直接再在next函数中添加跳转信息会引起无线循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
var queue = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(function (m) { return m.beforeEnter; }),
// async components
resolveAsyncComponents(activated)
);

this.pending = route;
var iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort()
}
try {
hook(route, current, function (to) {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this$1.ensureURL(true);
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort();
if (typeof to === 'object' && to.replace) {
this$1.replace(to);
} else {
this$1.push(to); //引起递归调用
}
} else {
// confirm transition and pass on the value
next(to);
}
});
} catch (e) {
abort(e);
}
};
}
History.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;

var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
pushState(cleanPath(this$1.base + route.fullPath));
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};
History.prototype.transitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;
var route = this.router.match(location, this.current);
this.confirmTransition(...)
}

在next具体路由后再调用next(),可实现中止导航,即暂停递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if(!hasMenus){
hasMenus = true
let temp = await createRoutes() //拿到数据
router.addRoutes(temp) //添加路由
if(!to.name){ //处理刷新,刷新时,当前页面name会变成null
next({name:to.path.substring(1)})
}
}
if (to.matched.some(res => res.meta.requireAuth)) {// 判断是否需要登录权限
cookies.getAuthorization().then(token=>{
if(!token){
next({
name: 'login',
params: {from: to.name}
})
}else{
next()
}
})
} else {
next() //终止递归
}

小结

多读源码总是用好处的

My Little World

多语言切换文字提取工具

发表于 2020-06-24

背景

项目想要添加语言切换功能,负责该任务的同事由于待翻译的文本提取工作量巨大原因产生进度问题
项目领导委派我帮忙进行辅助提取的工作

原理

i18n原理就是在全局挂载自己的具有翻译功能的方法,然后将需要翻译的文本作为参数传入,
然后函数根据当前语言环境,调用相应语言的翻译文件,通过key-value形式将对应语言的翻译结果返回给页面显示
项目中以简体中文作为key,翻译结果作为value形成翻译文件
如

1
2
3
4
5
6
7
zh-CN.js 简体中文
'开始日期': '开始日期',
'结束日期': '结束日期',

zh-TW.js 繁体中文
'开始日期': '開始日期',
'结束日期': '結束日期',

需要做的大量工作就是将项目中前端写的会展示在页面上的文本全部提取出来,形成文件,
并给相应的简体中文地方用i18n全局函数括起来,通过调用函数返回翻译结果

实现

因为同事使用纯手工作业,所以进度缓慢,按照这种做法,与其说自己比较懒,一个一个手动改,不如说自己觉得这种方式很low
一点都不酷,所以想起之前学习过的nodejs的知识,通过读取文件然后处理文件,最后生成新文件,用新文件替换老文件
处理文件过程会将简体中文提取出来形成文本提取文件,新文件中的简体中文会被i18n的全局函数包裹

读取

1
2
3
4
5
6
7
8
9
10
11
var fs = require('fs'); //引入文件处理模块
function read_file_sync(file_path) {
var data = fs.readFileSync(file_path, 'utf-8'); //读取文件
let arr = file_path.split('/')
let len = arr.length
let temp = arr[len-1].split('.')[0]
//获取文件名,如果待处理文件是子目录下的,保持生成的文本提取文件和新文件与原来文件保持相同目录结构
let fileName = arr[len-2] === 'console'?temp:arr.slice(4,arr.length-1).join('/')+'/'+temp //子目录情况
GetChinese(data,fileName)
}
read_file_sync('./src/components/console/san-manage.vue')

提取生成文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function GetChinese(strValue,fileName) { 
if(strValue=== null || strValue === ""){
return []
 }
let originData = strValue
let arr = getWords(originData) //提取简体中文
let obj = {}
arr.map(item=>{obj[item]=''})
let str = JSON.stringify(obj,''," ")
str = 'export default '+ str
//生成文本文件
fs.writeFile('./words/js/'+fileName+'.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});
//添加i18n全局函数,替换老文件,生成新文件
let index = originData.indexOf('<script>')
let template = originData.substring(0,index)//这里仅替换VUE文件的template模板部分,因为js部分有时涉及到拼接处理,单独处理
let templateResult = partTransfor(template) //得到替换结果
let other = originData.substring(index)//获取vue文件里面的js部分
//二者拼接写入新文件
fs.writeFile('./words/vue/'+fileName+'.vue', templateResult+other, function(err){
if(err) console.log('重写文件操作失败');
else console.log('重写文件操作成功');
});
}

提取

利用正则提取简体中文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getWords(strValue,flag){
//flag为true的时候,提取template部分简体中文,供替换使用
//flag为false的时候,提取全部简体中文,二者正则不同
var reg = flag? /\"?([0-9a-zA-Z\u4e00-\u9fa5|\-|\s|\(|\)\:\:\、\,\,\!\!\。])+/g : /(([0-9a-zA-Z]+)?[\u4e00-\u9fa5|\-|\s|\(|\)\、\,\,\:\:\!\!\。\~]([0-9a-zA-Z]+)?)+/g
//匹配注释的正则,注释不用提取翻译,故提取前先移除
let regCommon = /(\/\/[\w\s\,\,\‘\(\)\—\:\:\=\>\、\?\。a-zA-Z\.\u4e00-\u9fa5|\[|\]|-]*\n)|(\<\!\-\-[\w\s\‘\-\、a-zA-Z\u4e00-\u9fa5|\[|\]|-]*\-\-\>)|(\/\*[\w\‘\s\r\n\*\u4e00-\u9fa5|\-]*\*\/)/g
strValue = strValue.replace(regCommon, function(word) { // 去除注释后的文本
return ''
});
//优化提取结果,去重,过滤,排序
let res = [...new Set(strValue.match(reg))].filter(item=>{return /([\u4e00-\u9fa5])+/g.test(item)}).map(item=>{return item.trim()})
res = res.sort((a,b)=>{
return b.length-a.length
})
return res
}

去掉注释

本来想参考webpack打包时去除注释的正则,发现webpack去除注释,用的分词
直接通过判断astnode类型去判断是否为注释,然后根据配置决定保留去除
后来想起在阅读vue源码的时候有看到过在解析html的时候有形成注释类型的节点
所以从vue 源码中试图寻找注释的正则,发现vue也是只通过注释开头标志//,/** 或者<!–
来判断注释开始,所以我在此基础上自己编写了解析注释的正则并将他们去除

替换

替换文本,用括号括起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function partTransfor(strValue){
let originData = strValue
let res = getWords(strValue,true) //拿到template部分简体中文
for(let str of res){
let placeholder = str.charAt(0) ==='\"'
let temp = placeholder?str.substring(1):str
let reg1 = new RegExp(str,'ig')
let reg2 = new RegExp("placeholder=\"("+temp+")\"",'ig')
let reg = placeholder?reg2:reg1
originData = originData.replace(reg,function(word,str){
if(placeholder){ //如果是placeholder部分的简体中文
return ':placeholder="i18nTitle(\''+str+'\')"'
}else{
if(/[\u4e00-\u9fa5\:\']/.test(originData[str-1]) || /[\u4e00-\u9fa5\:\']/.test(originData[str+word])){ //对于一些包含特殊字符连接的手动处理
return word
}
return '{{i18nTitle(\''+word+'\')}}' //对于纯简体中文用函数包裹进行替换
}
})
}
return originData
}

其实需要处理的情况分三种

1
2
3
1.js里面用this.i18nTitle('xxx')
2.placeholder='xxx'改成:placehoder="i18nTitle('xxxx')"
3.单纯标签的这种,<label>xxxx</label>改成<label>{{i18nTitle(xxxx)}}</label>

另外涉及到组件里面的就直接在组件里需要展示的配置的中文简体字段变量外加全局函数

合并

每个文件提取处理好后,将所有字段合成一个文件,做去重处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var fs = require('fs');
var path = require('path');//解析需要遍历的文件夹
var filePath = path.resolve('./words/js');
var words = []
const getFileOfDirSync = (dir) => {
let files = fs.readdirSync(dir);
let result;
if (files) {
result = files.map((file) => {
let filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory()) {
return getFileOfDirSync(filePath); //递归
} else {
let content = fs.readFileSync(filePath, 'utf-8');
content = content.replace('export default','module.exports =') //更改模块编写方法
fs.writeFileSync(filePath,content);//方便读取
let data = require(filePath)
let keys = Object.keys(data)
let comment = content.substring(0,content.indexOf('module'))
return keys;
}
});
}
return [... new Set(result.flat(Infinity))]; //数组摊平去重
}
let res = getFileOfDirSync(filePath)
res = res.sort((a,b)=>{
return b.length-a.length
})
let obj = {}
res.map(item=>{obj[item]=''})
//res.map(item=>{obj[item]=item}) //直接生成简体中文翻译文件
let str = JSON.stringify(obj,''," ")
str = 'export default '+ str
fs.writeFile('./words/cnAll1.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});

翻译

根据简体中文和翻译结果形成翻译文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
let obj = {}
let ll = {}
let llkeys = Object.keys({ //翻译结果
'建議選擇購買當前雲裸機已掛載使用的存儲類型和磁盤類型':'',
'超高端存儲:穩定性和安全性最好,適用於核心業務系統':'',
'高端存儲:穩定性和安全性較好,適用於重要業務系統':'',
'中端存儲:穩定性和安全性壹般,適用於壹般業務系統':'',
'普通存儲:穩定性和安全性較差,適用於邊緣業務系統':'',
'NVME:新壹代固態磁盤,性能最好、延時最低,適用於核心業務系統':'',
'SSD:固態磁盤,性能較好、延時較低,適用於重要業務系統':'',
'SAS:機械磁盤,性能壹般、延時壹般,適用於壹般業務系統':'',
'SATA:機械磁盤,性能較差、延時較高,適用於邊緣業務系統和歸檔數據':'',
})
let objkeys = Object.keys({ //简体中文
'建议选择购买当前云裸机已挂载使用的存储类型和磁盘类型':'',
'超高端存储:稳定性和安全性最好,适用于核心业务系统':'',
'高端存储:稳定性和安全性较好,适用于重要业务系统':'',
'中端存储:稳定性和安全性一般,适用于一般业务系统':'',
'普通存储:稳定性和安全性较差,适用于边缘业务系统':'',
'NVME:新一代固态磁盘,性能最好、延时最低,适用于核心业务系统':'',
'SSD:固态磁盘,性能较好、延时较低,适用于重要业务系统':'',
'SAS:机械磁盘,性能一般、延时一般,适用于一般业务系统':'',
'SATA:机械磁盘,性能较差、延时较高,适用于边缘业务系统和归档数据':'',
})
let tw = {}
objkeys.map((item,i)=>{
tw[item] = llkeys[i] //生成繁体翻译文件
obj[item] = item //生成简体中文
})

let str = JSON.stringify(tw,''," ")
str = 'export default '+ str
fs.writeFile('zh-TW.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});

str = JSON.stringify(obj,''," ")
str = 'export default '+ str
fs.writeFile('zh-CN.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});

后续

有新页面上新需要对新老字段进行差值处理,仅新增原来没有的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let data = require('./words/zh-CN')
let keys = Object.keys(data) //老数据
let dataNew = require('./words/cnAll1')
let keysNew = Object.keys(dataNew)//新页面数据
let obj = {}
let newWords = keysNew.filter(newkey=>{
return !keys.includes(newkey)
}).map(item=>{
obj[item]=item
return item
})//得到新增的文本
let ll = {} //新增文本翻译结果
let llkeys = Object.keys(ll)
let objkeys = Object.keys(obj)
let tw = {}
objkeys.map((item,i)=>{
tw[item] = llkeys[i]
})
//生成简体和繁体翻译文件,再复制粘贴到老文件中
// let str = JSON.stringify(obj,''," ")
let str = JSON.stringify(tw,''," ")
str = 'export default '+ str
fs.writeFile('./words/compareResult3.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});

小结

其他云多语言切换

阿里云:首先会根据不同域名产生对应不同语言的站点,站点画面风格可能不同,如日本
另外根据语言切换,还会在同一域名下通过切换URI产生不同语言广告页面(切换时,请求cookie中会携带语言类型,aliyun_lang)
广告页面和站点页面可能相同,如香港,台湾的繁体页面,
可能不同,如简体中文和日文的时候

腾讯云:站点只有两种:中国站和国际站,两种站点风格不同
多语言切换在国际站点里面进行,同样通过切换URI,切换不同语言
切换时直接在query里面携带语言信息(https://intl.cloud.tencent.com/jp/?lang=jp&pg=)

平安云:只有两种语言切换,中文和英文,使用同一站点,
切换时通过在cookie里面携带需要的语言类型字段language,
获取相应语言的html和图片

i18n大致原理

i18n 的翻译原理就是在vue.use挂载i18n后,在install阶段会挂载上$t全局函数
我们在使用$t的时候只是单纯传入待翻译的简体中文,
$t会再调用VUEI18N对象上的内部方法,将当前语言环境和语言库传入,
然后通过message[key]的形式找到对应的翻译值,返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
Vue.prototype.$t = function (key) {
var values = [], len = arguments.length - 1;
while ( len-- > 0 ) values[ len ] = arguments[ len + 1 ];

var i18n = this.$i18n;
return i18n._t.apply(i18n, [ key, i18n.locale, i18n._getMessages(), this ].concat( values ))
};

VueI18n.prototype._getMessages = function _getMessages () { return this._vm.messages };

//this._vm.messages 值生成
VueI18n.prototype.setLocaleMessage = function setLocaleMessage (locale, message) {
if (this._warnHtmlInMessage === 'warn' || this._warnHtmlInMessage === 'error') {
this._checkLocaleMessage(locale, this._warnHtmlInMessage, message);
}
this._vm.$set(this._vm.messages, locale, message);
};

VueI18n.prototype.mergeLocaleMessage = function mergeLocaleMessage (locale, message) {
if (this._warnHtmlInMessage === 'warn' || this._warnHtmlInMessage === 'error') {
this._checkLocaleMessage(locale, this._warnHtmlInMessage, message);
}
this._vm.$set(this._vm.messages, locale, merge({}, this._vm.messages[locale] || {}, message));
};

VueI18n.prototype._t = function _t (key, _locale, messages, host) {
var ref;

var values = [], len = arguments.length - 4;
while ( len-- > 0 ) values[ len ] = arguments[ len + 4 ];
if (!key) { return '' }

var parsedArgs = parseArgs.apply(void 0, values);
var locale = parsedArgs.locale || _locale;

var ret = this._translate(
messages, locale, this.fallbackLocale, key,
host, 'string', parsedArgs.params
);
if (this._isFallbackRoot(ret)) {
if (!this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
warn(("Fall back to translate the keypath '" + key + "' with root locale."));
}
/* istanbul ignore if */
if (!this._root) { throw Error('unexpected error') }
return (ref = this._root).$t.apply(ref, [ key ].concat( values ))
} else {
ret = this._warnDefault(locale, key, ret, host, values, 'string');
if (this._postTranslation) {
ret = this._postTranslation(ret);
}
return ret
}
};

VueI18n.prototype._translate = function _translate (
messages,
locale,
fallback,
key,
host,
interpolateMode,
args
) {
//messages[locale] 拿到语言库
var res =
this._interpolate(locale, messages[locale], key, host, interpolateMode, args, [key]);
if (!isNull(res)) { return res }

res = this._interpolate(fallback, messages[fallback], key, host, interpolateMode, args, [key]);
if (!isNull(res)) {
if (!this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
warn(("Fall back to translate the keypath '" + key + "' with '" + fallback + "' locale."));
}
return res
} else {
return null
}
};

VueI18n.prototype._interpolate = function _interpolate (
locale,
message,
key,
host,
interpolateMode,
values,
visitedLinkStack
) {
if (!message) { return null }

var pathRet = this._path.getPathValue(message, key);
if (Array.isArray(pathRet) || isPlainObject(pathRet)) { return pathRet }

var ret;
if (isNull(pathRet)) {
/* istanbul ignore else */
if (isPlainObject(message)) {
ret = message[key]; //从语言库里面拿到对应翻译值
if (typeof ret !== 'string') {
if (!this._isSilentTranslationWarn(key) && !this._isSilentFallback(locale, key)) {
warn(("Value of key '" + key + "' is not a string!"));
}
return null
}
} else {
return null
}
} else {
/* istanbul ignore else */
if (typeof pathRet === 'string') {
ret = pathRet;
} else {
if (!this._isSilentTranslationWarn(key) && !this._isSilentFallback(locale, key)) {
warn(("Value of key '" + key + "' is not a string!"));
}
return null
}
}

// Check for the existence of links within the translated string
if (ret.indexOf('@:') >= 0 || ret.indexOf('@.') >= 0) {
ret = this._link(locale, message, ret, host, 'raw', values, visitedLinkStack);
}

return this._render(ret, interpolateMode, values, key)
};

VueI18n.prototype._render = function _render (message, interpolateMode, values, path) {
var ret = this._formatter.interpolate(message, values, path);
// If the custom formatter refuses to work - apply the default one
if (!ret) {
ret = defaultFormatter.interpolate(message, values, path);
}
// if interpolateMode is **not** 'string' ('row'),
// return the compiled data (e.g. ['foo', VNode, 'bar']) with formatter
return interpolateMode === 'string' && typeof ret !== 'string' ? ret.join('') : ret
};

My Little World

KOA框架剥洋葱原理

发表于 2020-06-24

收集

new 一个koa实例后,调用use接口会将传入的中间件函数push到middleware属性数组中做收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
application.js

use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

根据koa框架版本不同,需要将使用generator函数写的中间件转化成基于promise的函数形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const convert = require('koa-convert');

function convert (mw) {
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
//再次确认是否是generator函数
if (mw.constructor.name !== 'GeneratorFunction') {
// 假设它是一个基于promise的中间件
// 就直接返回
return mw
}
//如果是generator函数就转换成promise形式
const converted = function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}

const co = require('co')

function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)

// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();

/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/

function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* @param {Error} err
* @return {Promise}
* @api private
*/

function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/

function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}

整合

调用listen函数开启服务时,整合所有中间件为一个大函数,大函数中进行剥洋葱流程
在收到请求时调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
application.js

listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

callback() {
const fn = compose(this.middleware); //整合中间件

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn); //调用中间件
};

return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
//通过传递过来的ctx,获取到原生的可写流
const res = ctx.res;
//设置默认状态码
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
//调用中间件大函数 同时准备catch处理中间件级错误
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

整合中间件 ,返回大函数,大函数中有剥洋葱模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
//调用时fnMiddleware(ctx).then(handleResponse).catch(onerror);
//next 是undefined
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {//try-catch 用于保证错误在promise的情况能够正常捕获
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

剥洋葱流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let index = -1
return dispatch(0) //从第一个中间件开始执行
function dispatch (i) {
//防止next 在一个中间件中被调用多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i] //拿到中间件
if (i === middleware.length) fn = next //如果中间件函数全部调用完,fn 赋值为next,next为undefined
if (!fn) return Promise.resolve() //fn为next=>undefined时成立,即执行到最后返回一个Promise.resolve() 可以继续写then
try {//try-catch 用于保证错误在promise的情况能够正常捕获
//执行中间件函数,传入ctx,next参数,next即dispatch.bind(null, i + 1),
//递归调用下一个中间件函数
//从这一步可以实现await next()时,先执行下一个中间件函数
//中间件有调用next,就利用await暂停当前中间件执行,开始执行下一个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}

利用await暂停当前中间件执行,调用next开始执行下一个中间件

如果下一个中间件没有调next,且不是最后一个中间件,
即不会再调用dispatch执行下下一个中间件,则后续中间件都不会被执行到

如果下一个中间件是最后一个中间件,且在其中调用了next,则不会走到这个流程,
在上面的流程中就return

如果下一个中间件是最后一个中间件,则执行完return Promise.resolve
上一个中间件await 拿到结果后继续执行await后面的代码
执行完await后面代码后,上上一个中间件await 拿到结果,继续执行当前中间件await后代码
依次类推,直到第一个中间件await后代码执行完毕,整个中间件流程,
即剥洋葱流程先内层后外层执行流程完毕
返回promise 接着then处理响应或者中间有错误捕获错误
fnMiddleware(ctx).then(handleResponse).catch(onerror)

My Little World

vuex 源码学习

发表于 2020-04-27
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
let moduleA = {
namespaced: true,
state(){
return{
counta: 0, //分别在模块b和全局下被使用,直接返回对象会通过引用被共享,防止模块间数据污染,使用函数,每次生成新对象,原理同vue的data
}
},
getters:{
getCount(...args){ //this.$store.getters['b/aa/getCount'] 作为b子模块时
console.log(args)
return 'this is a getter~~~~~'
}
},
mutations: { // this.$store.commit('b/aa/increment') 仅触发模块b下面的aa子模块
increment (state) { //this.$store.commit('a/increment') 仅触发全局的模块a 对应的子模块
state.counta++
},
incrementaaa (state) { //在被引用时,套嵌层如果都没有namespaced: true,可以直接用this.$store.commit('incrementaaa')调用进行更改
state.counta++
}
}
}

let moduleB = {
namespaced: true,
state: {
countb: 0,//this.$store.state.b.countb
},
getters:{
getCount(...args){ //this.$store.getters['b/getCount'] 因为设置了namespaced所以可以重名
console.log(args)
return 'this is a getter~~~~~'
}
},
mutations: {
increment (state) { //如果不加namespaced,执行this.$store.commit('increment'),会同时执行
state.countb++
}
},
modules: {//如果moduleB不加namespaced,aa可以访问数据但不能调用this.$store.commit('b/aa/increment')进行更改
aa: moduleA,//this.$store.state.b.aa.counta
}
}
// vuex相关代码
const store = new Vuex.Store({
state: {
count: 0, //this.$store.state.count
},
getters:{
getCount(...args){ //this.$store.getters.getCount
console.log(args)
return 'this is a getter'
}
},
mutations: {
increment (state) { //this.$store.commit('increment') 子模块有相同函数时,都会触发执行
state.count++
}
},
modules: {
a: moduleA, //this.$store.state.a.counta
b: moduleB //this.$store.state.b.countb
}
})

以上面store结构为例,分析vuex源码

new Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var Store = function Store (options) {
var this$1 = this;
if ( options === void 0 ) options = {};
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
var strict = options.strict; if ( strict === void 0 ) strict = false;

// 一些内部参数
this._committing = false;
this._actions = Object.create(null);
this._actionSubscribers = [];
this._mutations = Object.create(null);
this._wrappedGetters = Object.create(null);
this._modules = new ModuleCollection(options);
this._modulesNamespaceMap = Object.create(null);
this._subscribers = [];
this._watcherVM = new Vue();
this._makeLocalGettersCache = Object.create(null);

//绑定 commit 和 dispatch 的执行对象始终指向自己,防止外界重新绑定
var store = this;
var ref = this;
var dispatch = ref.dispatch;
var commit = ref.commit;
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
};
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
};

// 严格模式
//使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
this.strict = strict;
//拿到处理后的state
var state = this._modules.root.state;

// 初始化全局module
// 同时递归注册所有子模块
// 收集this._wrappedGetters中的所有模块的getters
installModule(this, state, [], this._modules.root);

// 初始化store vm, 用于数据响应
// 同时注册 _wrappedGetters 作为计算属性)
resetStoreVM(this, state);

// 使用插件处理
plugins.forEach(function (plugin) { return plugin(this$1); });
//options.devtools为某个特定的 Vuex 实例打开或关闭 devtools。
//对于传入 false 的实例来说 Vuex store 不会订阅到 devtools 插件。可用于一个页面中有多个 store 的情况
var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
devtoolPlugin(this);
}
};

install

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function install (_Vue) {
if (Vue && _Vue === Vue) {
//Vue.use(Vuex) should be called only once
return
}
Vue = _Vue;
applyMixin(Vue);
}
function applyMixin (Vue) {
var version = Number(Vue.version.split('.')[0]);

if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit }); //使用beforeCreate将$store绑定到每个VueComponent 上
} else {
//小于版本2的用_init初始化
var _init = Vue.prototype._init;
Vue.prototype._init = function (options) {
if ( options === void 0 ) options = {};

options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit;
_init.call(this, options);
};
}

function vuexInit () {
var options = this.$options;
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store; //当前组件$store指向父组件$store,递归指向,从而保证唯一性
}
}
}

ModuleCollection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var ModuleCollection = function ModuleCollection (rawRootModule) {
// 根据new Store 传入的参数,注册根模块
this.register([], rawRootModule, false);
};
// new store 时传入的参数 [], rawRootModule, false
ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
var this$1 = this;
if ( runtime === void 0 ) runtime = true;
var newModule = new Module(rawModule, runtime);
//{runtime:false,_children:{},_rawModule:rawModule,state:rawModule上面的state,__proto__:一系列方法}
if (path.length === 0) {
this.root = newModule; //初始化根模块
} else { //初始化子模块
var parent = this.get(path.slice(0, -1)); //拿到父模块
parent.addChild(path[path.length - 1], newModule); //给父模块_children属性增加子模块
}

// 注册嵌套模块
if (rawModule.modules) {
forEachValue(rawModule.modules, function (rawChildModule, key) {
this$1.register(path.concat(key), rawChildModule, runtime); //递归注册
});
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Module = function Module (rawModule, runtime) {
this.runtime = runtime;
// Store some children item
this._children = Object.create(null);
// Store the origin module object which passed by programmer
this._rawModule = rawModule;
var rawState = rawModule.state;

// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {};
};

Module.prototype.addChild = function addChild (key, module) {
this._children[key] = module;
};
Module.prototype.getChild = function getChild (key) {
return this._children[key]
};

所以这一步结束后

1
this._modules = new ModuleCollection(options);

this._modules为如下对象

1
2
3
4
5
6
7
8
9
10
ModuleCollection {
root: Module {
runtime: false,
_children: {a: Module, b: Module}
_rawModule: {state: {…}, mutations: {…}, modules: {…}}
state: {count: 0}
__proto__: Object
}
__proto__: Object
}

ModuleCollection对象上的其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ModuleCollection.prototype.get = function get (path) {
return path.reduce(function (module, key) {
return module.getChild(key)
}, this.root)
};

ModuleCollection.prototype.getNamespace = function getNamespace (path) {
var module = this.root;
//对设置了namespaced的模块进行拼接
return path.reduce(function (namespace, key) {
module = module.getChild(key); //从_children取出子模块
return namespace + (module.namespaced ? key + '/' : '')
}, '')
};

ModuleCollection.prototype.update = function update$1 (rawRootModule) {
update([], this.root, rawRootModule);
};
ModuleCollection.prototype.unregister = function unregister (path) {
var parent = this.get(path.slice(0, -1));
var key = path[path.length - 1];
if (!parent.getChild(key).runtime) { return }

parent.removeChild(key);
};

installModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//初始化调用时传参 (this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) {
//根模块时,参数分别为store本身,根模块state, [], 根模块,undefined
//递归子模块时,参数为 store本身,根模块state,子模块在modules对象中的路径key值
var isRoot = !path.length; //空数组即根模块
//ModuleCollection.prototype.getNamespace 根模块返回空字符串''
var namespace = store._modules.getNamespace(path);

//如果模块设置了命名空间 在store._modulesNamespaceMap属性中注册
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && "development" !== 'production') {
console.error(("[vuex] duplicate namespace " + namespace + " for the namespaced module " + (path.join('/'))));
}
store._modulesNamespaceMap[namespace] = module;
}

//对子模块state进行数据劫持处理
if (!isRoot && !hot) {
var parentState = getNestedState(rootState, path.slice(0, -1));
var moduleName = path[path.length - 1];
store._withCommit(function () {
{
if (moduleName in parentState) {
console.warn(
("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"")
);
}
}
Vue.set(parentState, moduleName, module.state); //将子模块state按照模块名添加到父模块state中
});
}
//根据有无namespace
//截取state,getter的get,
//封装触发函数dispactch和commit,有namespace拼接后再触发
var local = module.context = makeLocalContext(store, namespace, path);

module.forEachMutation(function (mutation, key) {
var namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});

module.forEachAction(function (action, key) {
var type = action.root ? key : namespace + key;
var handler = action.handler || action;
registerAction(store, type, handler, local);
});

module.forEachGetter(function (getter, key) {
var namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});

module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});
}

forEachValue

公共方法

1
2
3
function forEachValue (obj, fn) {
Object.keys(obj).forEach(function (key) { return fn(obj[key], key); });
}

注册 Mutation

1
2
3
4
5
6
7
8
9
10
11
12
Module.prototype.forEachMutation = function forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn);
}
};
//参数:store本身,经过namespace拼接后的类型标记,具体对应的mutation,当前模块上封装好的state,getters,commit,dispatch
function registerMutation (store, type, handler, local) {
var entry = store._mutations[type] || (store._mutations[type] = []);
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload);
});
}

小结:将当前模块的mutations上的各个mutations结合namespace存储到store._mutations属性上
可见,如果子模块没有namespace,同名的mutation会跟根模块的mutation存储在相同的type下面
所以当触发commit的时候会一起执行,
如果子模块设置了namespace,这时存储的type会包含该模块对应的名称,
即使同名的mutation也被存放在不同的type中,实现了隔离

注册 Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Module.prototype.forEachAction = function forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn);
}
};

function registerAction (store, type, handler, local) {
var entry = store._actions[type] || (store._actions[type] = []);
entry.push(function wrappedActionHandler (payload) {
var res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload);
if (!isPromise(res)) {
res = Promise.resolve(res);
}
if (store._devtoolHook) {
return res.catch(function (err) {
store._devtoolHook.emit('vuex:error', err);
throw err
})
} else {
return res
}
});
}

小结:将当前模块的actions上的各个action结合namespace存储到store._actions属性上

注册 Getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Module.prototype.forEachGetter = function forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn);
}
};

function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
{
console.error(("[vuex] duplicate getter key: " + type));
}
return
}
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // 当前模块state
local.getters, // 当前模块 getters
store.state, // 根模块 state
store.getters // 根模块 getters
)
};
}

小结:将当前模块的getters上的各个getter结合namespace存储到store._wrappedGetters属性上

注册 Module

递归调用installModule注册子模块

1
2
3
4
5
6
7
Module.prototype.forEachChild = function forEachChild (fn) {
forEachValue(this._children, fn);
};

module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});

makeLocalContext

优化 dispatch, commit, getters and state
如果没有设置namespace, 就使用根模块的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
  function makeLocalContext (store, namespace, path) {
var noNamespace = namespace === '';

var local = {
dispatch: noNamespace ? store.dispatch : function (_type, _payload, _options) {
var args = unifyObjectStyle(_type, _payload, _options);
var payload = args.payload;
var options = args.options;
var type = args.type;

if (!options || !options.root) {
type = namespace + type;//有namespace拼接后再触发
if (!store._actions[type]) {
console.error(("[vuex] unknown local action type: " + (args.type) + ", global type: " + type));
return
}
}

return store.dispatch(type, payload)
},

commit: noNamespace ? store.commit : function (_type, _payload, _options) {
var args = unifyObjectStyle(_type, _payload, _options);
var payload = args.payload;
var options = args.options;
var type = args.type;

if (!options || !options.root) {
type = namespace + type; //有namespace拼接后再触发
if (!store._mutations[type]) {
console.error(("[vuex] unknown local mutation type: " + (args.type) + ", global type: " + type));
return
}
}

store.commit(type, payload, options);
}
};

// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? function () { return store.getters; }
: function () { return makeLocalGetters(store, namespace); }
},
state: {
get: function () { return getNestedState(store.state, path); }
}
});

return local
}

function makeLocalGetters (store, namespace) {
if (!store._makeLocalGettersCache[namespace]) {
var gettersProxy = {};
var splitPos = namespace.length;
Object.keys(store.getters).forEach(function (type) {
// skip if the target getter is not match this namespace
if (type.slice(0, splitPos) !== namespace) { return }

// extract local getter type
var localType = type.slice(splitPos);

// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
Object.defineProperty(gettersProxy, localType, {
get: function () { return store.getters[type]; },
enumerable: true
});
});
store._makeLocalGettersCache[namespace] = gettersProxy;
}

return store._makeLocalGettersCache[namespace]
}
function getNestedState (state, path) {
return path.reduce(function (state, key) { return state[key]; }, state)
}

小结

该步骤完成后

store对象长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
commit: ƒ boundCommit(type, payload, options)
dispatch: ƒ boundDispatch(type, payload)
strict: false
_actionSubscribers: []
_actions: {} //存放所有模块的action
_committing: false
_makeLocalGettersCache: {}
_modules: ModuleCollection {
root: Module
context: //新增根据namespace处理后的触发函数
commit: ƒ boundCommit(type, payload, options)
dispatch: ƒ boundDispatch(type, payload)
getters: (...)
state: (...)
get getters: ƒ ()
get state: ƒ ()
__proto__: Object
runtime: false
state: //将子模块state集中到根模块state
a: {counta: 2329}
b:
aa: {counta: 2329}
countb: 0
__proto__: Object
count: 0
__proto__: Object
_children: //对子模块递归过程挂载context属性
a: Module {runtime: false, _children: {…}, _rawModule: {…}, state: {…}, context: {…}}
b: Module {runtime: false, _children: {…}, _rawModule: {…}, state: {…}, context: {…}}
_rawModule: {state: {…}, mutations: {…}, modules: {…}}
namespaced: (...)
__proto__: Object
__proto__: Object
}
_modulesNamespaceMap: {b/: Module} //存放设置了namespace的模块
_mutations: {increment: Array(1), incrementaaa: Array(1), b/increment: Array(1), b/incrementaaa: Array(1)} //存放所有模块的mutation
_subscribers: []
_watcherVM: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
_wrappedGetters: {} //存放所有模块的getters
state: (...) //对局部变量做了处理,还没有挂到store上

resetStoreVM

利用vue的数据处理逻辑,解决getters和state之间订阅发布关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function partial (fn, arg) {
return function () {
return fn(arg)
}
}
function resetStoreVM (store, state, hot) {
var oldVm = store._vm;
//绑定存储公共的getters
store.getters = {};
// 重置内部getters缓存到_makeLocalGettersCache
store._makeLocalGettersCache = Object.create(null);
var wrappedGetters = store._wrappedGetters;
var computed = {};
forEachValue(wrappedGetters, function (fn, key) {
//如果直接使用,闭包里面会包含oldVm
computed[key] = partial(fn, store);
Object.defineProperty(store.getters, key, {
get: function () { return store._vm[key]; },//直接调用vue的compute属性
enumerable: true // for local getters
});
});

//使用vue实例存放state和getters
var silent = Vue.config.silent;
/* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed: computed
});
Vue.config.silent = silent;

/* 使能严格模式,保证修改store只能通过mutation */
if (store.strict) {
enableStrictMode(store);
}

if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(function () {
oldVm._data.$$state = null;
});
}
Vue.nextTick(function () { return oldVm.$destroy(); });
}
}

store上的其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
Store.prototype.commit = function commit (_type, _payload, _options) {
var this$1 = this;

// check object-style commit
var ref = unifyObjectStyle(_type, _payload, _options);
var type = ref.type;
var payload = ref.payload;
var options = ref.options;

var mutation = { type: type, payload: payload };
var entry = this._mutations[type];
if (!entry) {
{
console.error(("[vuex] unknown mutation type: " + type));
}
return
}
this._withCommit(function () {
entry.forEach(function commitIterator (handler) {
handler(payload);
});
});

this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(function (sub) { return sub(mutation, this$1.state); });

if (
options && options.silent
) {
console.warn(
"[vuex] mutation type: " + type + ". Silent option has been removed. " +
'Use the filter functionality in the vue-devtools'
);
}
};

Store.prototype.dispatch = function dispatch (_type, _payload) {
var this$1 = this;

// check object-style dispatch
var ref = unifyObjectStyle(_type, _payload);
var type = ref.type;
var payload = ref.payload;

var action = { type: type, payload: payload };
var entry = this._actions[type];
if (!entry) {
{
console.error(("[vuex] unknown action type: " + type));
}
return
}

try {
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(function (sub) { return sub.before; })
.forEach(function (sub) { return sub.before(action, this$1.state); });
} catch (e) {
{
console.warn("[vuex] error in before action subscribers: ");
console.error(e);
}
}

var result = entry.length > 1
? Promise.all(entry.map(function (handler) { return handler(payload); }))
: entry[0](payload);

return result.then(function (res) {
try {
this$1._actionSubscribers
.filter(function (sub) { return sub.after; })
.forEach(function (sub) { return sub.after(action, this$1.state); });
} catch (e) {
{
console.warn("[vuex] error in after action subscribers: ");
console.error(e);
}
}
return res
})
};

Store.prototype.subscribe = function subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
};

Store.prototype.subscribeAction = function subscribeAction (fn) {
var subs = typeof fn === 'function' ? { before: fn } : fn;
return genericSubscribe(subs, this._actionSubscribers)
};

Store.prototype.watch = function watch (getter, cb, options) {
var this$1 = this;

{
assert(typeof getter === 'function', "store.watch only accepts a function.");
}
return this._watcherVM.$watch(function () { return getter(this$1.state, this$1.getters); }, cb, options)
};

Store.prototype.replaceState = function replaceState (state) {
var this$1 = this;

this._withCommit(function () {
this$1._vm._data.$$state = state;
});
};

Store.prototype.registerModule = function registerModule (path, rawModule, options) {
if ( options === void 0 ) options = {};

if (typeof path === 'string') { path = [path]; }

{
assert(Array.isArray(path), "module path must be a string or an Array.");
assert(path.length > 0, 'cannot register the root module by using registerModule.');
}

this._modules.register(path, rawModule);
installModule(this, this.state, path, this._modules.get(path), options.preserveState);
// reset store to update getters...
resetStoreVM(this, this.state);
};

Store.prototype.unregisterModule = function unregisterModule (path) {
var this$1 = this;

if (typeof path === 'string') { path = [path]; }

{
assert(Array.isArray(path), "module path must be a string or an Array.");
}

this._modules.unregister(path);
this._withCommit(function () {
var parentState = getNestedState(this$1.state, path.slice(0, -1));
Vue.delete(parentState, path[path.length - 1]);
});
resetStore(this);
};

Store.prototype.hotUpdate = function hotUpdate (newOptions) {
this._modules.update(newOptions);
resetStore(this, true);
};

Store.prototype._withCommit = function _withCommit (fn) {
var committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
};

function genericSubscribe (fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn);
}
return function () {
var i = subs.indexOf(fn);
if (i > -1) {
subs.splice(i, 1);
}
}
}

function resetStore (store, hot) {
store._actions = Object.create(null);
store._mutations = Object.create(null);
store._wrappedGetters = Object.create(null);
store._modulesNamespaceMap = Object.create(null);
var state = store.state;
// init all modules
installModule(store, state, [], store._modules.root, true);
// reset vm
resetStoreVM(store, state, hot);
}
My Little World

vue 源码学习四【数据双向绑定】

发表于 2020-04-11

vue双向绑定原理,依赖收集是
在created声明周期之前,
render生成虚拟dom的时候

1
2
3
4
5
6
7
8
9
Vue.prototype._init = function (options) {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
callHook(vm, 'beforeCreate');//运行订阅了beforecreate钩子的相关方法
initState(vm);//处理数据
callHook(vm, 'created');

转换

转换成内置函数mergedInstanceDataFn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
function mergeOptions (
parent,
child,
vm
) {
{
checkComponents(child);
}

if (typeof child === 'function') {
child = child.options;
}

// ... normalizeProps, normalizeInject, normalizeDirectives

if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}

var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
//在上面的循环中会遍历到配置的options参数里的data属性,则会调用到strats.data
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
//
strats.data = function (
parentVal,
childVal,
vm
) {
if (!vm) {
//...vm就是VUE实例,所以不会走这里
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
};

function mergeDataOrFn (
parentVal,
childVal,
vm
) {
if (!vm) {
...
} else {
return function mergedInstanceDataFn () {
// instance merge
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}

这一步结束后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vm.$options.data = function mergedInstanceDataFn () {
//声明时传递进来的data属性值,是函数则执行拿到对象
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
//内部属性没有data属性,所以 defaultData 为undefined
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
function mergeData (to, from) {
if (!from) { return to }
...//省略若干逻辑
}

数据劫持

开始进行真正初始化—数据劫持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
}

function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
//得到的data不是object对象
if (!isPlainObject(data)) {
data = {};
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{if (methods && hasOwn(methods, key)) {...与method同名警告}}
if (props && hasOwn(props, key)) {
...与prop同名警告
} else if (!isReserved(key)) { //不是以$或者_开头的key
proxy(vm, "_data", key); //将key值存一份到vm的'_data'属性上
}
}
// observe data
observe(data, true /* asRootData */);
}

function getData (data, vm) {
// #7573 disable dep collection when invoking(调用) data getters
pushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}

给data创建观察者实例,挂载‘_ob_’属性,指向一个Observer对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}

Observer对象有三个属性,‘ob’属性相同
{
value:data,
dep : new Dep();
vmCount :0
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
//遍历所有属性并将它们转换为getter / setter。
//仅当值类型为Object时才应调用此方法。
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};

真正进行劫持的方法defineReactive$$1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();

var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}

// cater(迎合) for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}

var childOb = !shallow && observe(val);//对值进行劫持处理 开始递归处理
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) { //new watch的时候,Dep.target指向Watcher对象,再运行render函数,访问具体变量就会调用这里
dep.depend();//对key的依赖进行依赖收集
if (childOb) {//如果值是一个对象的情况下
childOb.dep.depend();//对对象值的依赖进行依赖收集 实现深度监听
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
//相同值,不更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
//有setter方法直接用setter方法更新,没有直接赋值
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
//对新值进行劫持处理 更新childOb
//如果新值是对象对新赋的值在更新时进行依赖收集
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}

每个被劫持过的数据的标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};

Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this); //调用Watcher.addDep方法,this指dep
}
};

Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update(); //执行watcher的update函数
}
};
Dep.target = null;
var targetStack = [];

订阅发布

mountComponent函数会new Watcher一个对象,执行get方法【详见源码分析二】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
var _Set;
/* istanbul ignore if */ // $flow-disable-line
if (typeof Set !== 'undefined' && isNative(Set)) {
// use native Set when available.
_Set = Set;
} else {
// a non-standard Set polyfill that only works with primitive keys.
_Set = /*@__PURE__*/(function () {
function Set () {
this.set = Object.create(null);
}
Set.prototype.has = function has (key) {
return this.set[key] === true
};
Set.prototype.add = function add (key) {
this.set[key] = true;
};
Set.prototype.clear = function clear () {
this.set = Object.create(null);
};
return Set;
}());
}
var Watcher = function Watcher (...args){
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
}
Watcher.prototype.get = function get () {
pushTarget(this); //将当前Watcher挂到第三方Dep.target上
var value;
var vm = this.vm;
try {
//this.getter会执行render,触发数据的get方法,进行相互订阅
value = this.getter.call(vm, vm);
} catch (e) {
...
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
//watcher和dep互相存id
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this); //将watcher加入到dep的sub属性中
}
}
};
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get(); //重新调用render函数更新数据
if (
value !== this.value ||
//对于需要深度监听的数据类型,值没有变,也许值内容发生了变化
isObject(value) ||
this.deep
) {
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};

vue3.0数据响应式原理

vue3.0的改进

1.对源码进行优化
使用monorepo和TS管理和开发源码,提升自身代码的可维护性
2.性能优化
减少源码体积
移除冷门的feature(如filter,inline-template)
引入tree-shaking,减小打包体积

数据劫持优化
原来缺点
无法判断对象属性删除新增
深度层次结点要递归遍历,也不知道会不会用到,都要劫持,不友好

解决
使用proxy API 可以检测到删除还是新增
在用到时才劫持,避免无脑递归

3.编译优化
判断更新的颗粒度变小,从原来的组件级,利用block tree的区块细化到动态节点级
引入 compositionAPI 优化逻辑关注点相关的代码,可以避免mixin的时候的命名冲突

My Little World

sso单点登录实现

发表于 2020-03-25

背景

多项目想要实现统一登录,即登录一个系统后,在打开其他相关系统页面后,是已经登录的状态,不再进行登录

原理

多个系统使用统一的顶级域名,利用cookie的domain属性,将登录后cookie保存在浏览器中,只要是该cookie的domain所包含的域名的站点,都可以拿到这个cookie

实现

选择原来多个站点中其中一个站点的登录页,作为公共跳转页
在登录之后将token塞到浏览器中
这里设置统一token在cookie中的key值为_a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
根据运行环境设置domain,sit,dev均为测试环境
function getDomain(name){
let testENVs = ['localhost','sit','dev']
let testENV = ''
testENVs.forEach(env=>{
if(location.hostname.includes(env)){ //根据域名判断环境
testENV = env
}
})
let temp = testENV?testENV +'.xxxx.com':'xxxx.com'
//本地开发不加domain,默认localhost,否则本地没办法进行开发
let domain = testENV === 'localhost' ? '': (name === '_a'? temp:location.hostname)//个别cookie字段仅用于本站点,所以domain设置为站点域名
return domain
}
setCookie=(name,value) => {
const Days = 0.5
const exp = new Date()
exp.setTime(exp.getTime() + Days*24*60*60*1000)
let str = name + '=' + escape(value) + ';expires=' + exp.toGMTString()
let domain = getDomain(name)
if(domain){
str +=';domain=' + domain
}
document.cookie = str
},

之所以要根据不同运行环境设置不同domain,是因为要在不同测试环境也要实现单点登录
比如测试环境在sit环境的a.sit.xxxx.com和b.sit.xxxx.com
a.sit.xxxx.com登录页作为公共登录页
在a.sit.xxxx.com登录后,token的cookie会被设置成
key是_a,domain是sit.xxxx.com(浏览器会自动在前面加一个点),这样打开b.sit.xxxx.com的页面,浏览器中还会存在_a这个cookie,b.sit.xxxx.com就可以直接拿这个cookie向服务器发请求了

以此类推,在dev环境,这两个站点域名会变成
a.dev.xxxx.com和b.dev.xxxx.com
_a的domain会被设置成dev.xxxx.com
这时如果去b.dev.xxxx.com,_a是会有的
但去a.sit.xxxx.com,是不会有的,因为_a此时仅限于dev.xxxx.com域名及其子域名

等到了生产环境,二者域名变为
a.xxxx.com和b.xxxx.com
_a的domain会被设置成xxxx.com
此时就会出现一个问题,假如我先登录了线上环境
有了domain为xxxx.com的_a
再去测试环境登录,此时会加进去domain为sit/dev.xxx.com的_a
两个_a如何区分哪个才是当前环境的_a?

遇到的问题一 :测试环境_a加不进去,出现一闪而过
开始以为是浏览器机制问题,后来才发现,是代码逻辑问题
其实cookie是有写进浏览器的,跟cookie本身处理机制无关,可以写进去,问题出在了取cookie _a再给后台发请求的时候,以前是保存token的只有一个特殊key名,直接取那个key名就行,现在有两个_a,逻辑还是按照第一个来取,结果取错了,后台给发过来401,前端代码处理http状态码的逻辑又是401清cookie跳登录,所以domain为. sit/dev. xxxx. com的cookie就又消失了😅

遇到问题二 :两个_a如何区分哪个才是当前环境的_a?
因为后台实现分层,与前端直接对接的后台,无法访问后台的用户cookie表,能拿到cookie表的后台站在大局角度看问题,这个逻辑又觉得不是有必要加的,所以不能要求后台拿到报文中cookie后去表里面做判断
所以由前端通过读取document.cookie,整理拿到两个_a后,依次用每个_a去给后台发请求,如果成功了,说明当前_a是正确的,就可以用这个_a去发真正的请求,同时记录下这个_a,避免之后的重复探测

小结

1.碰到问题一时,很无厘头,但分析出原因后,让人感觉很无语,正确的说是,让人很没面子,遇到问题应该善于分析,戒骄戒躁
2.问题二,有时候,你认为很恶心的做法,恰巧可能会被采纳

后记

发版到线上后,发现部分同事登陆成功后又跳回登录页面,中间发起的请求报401

经过查看同事浏览器的cookie发现,存在设置了httponly的_a,即我们所有系统统一使用的token标识,
cookie设置了httponly意味着js没有权限进行获取更改甚至不能删除,所以新登录的token是没有写进去的,拿到的token也就是错的
解决办法就是让后台同事强制写一个没有httponly的_a进去,让后台覆盖后台,
说明浏览器优先级是先处理set-cookie的cookie逻辑,再看是否有httponly,决定js是否有权限处理cookie

相关链接
Cookie写不进去问题深入调查 https Secure Cookie
js创建cookie时获取一级域名设置domain解决跨域问题
js与cookie的domain和path之间的关系
前端单点登录(SSO)实现方法(二级域名与主域名)

My Little World

使用vuepress构建文档系统

发表于 2020-03-24

背景

面向普通用户的应用系统,包含很多产品的使用说明,属于静态页面
一方面占据整体项目的体积,打包时间用时长,打包结果体积大
另一方面,不断新增的不同产品的使用说明,对于前端开发人员来说,
相对开发真正的产品功能来说优先级较低,且占用开发时间

所以希望将产品使用说明与当前项目隔离,建立静态网站,存放使用说明
再由原系统进行跳转
将新增使用说明的任务交由相关产品负责人等非开发人员进行新增

问题

1.对于非开发人员来说,选择何种文档格式,可以既方便编写,又可以轻松转成html
2.原有系统用vue编写的批量文件如何迁移到新的静态网站

知识

vuepress

vuepress是一款使用vue驱动的静态网站生成器
用户可以使用markdown格式文件编辑文本,然后转义成html
另外页面中功能还可以使用vue进行嵌入

批量迁移

原系统vue编写的批量页面含有vue语法,不能直接迁移
所以需要将生成好的html爬下来转成md文件,应用到新系统
‘html-to-md’的npm 包可以帮助将html转换成md

解决

vuepress搭建

因为要与原系统主题风格样式类似,故需要在原来主题代码上进行修改
安装好vuepress包之后
将包里面的默认主题@vuepress/theme-default复制粘贴到doc/.vuepress/theme文件夹下面
修改里面的代码就可以直接应用了

配置config.js文件

在.vuepress文件夹下新增config.js

1
2
3
4
5
6
7
8
title:标签页名称
head:在页面头部引入的文件
base:当不在域名根目录下部署时,配置部署路径
themeConfig:{ 主题配置
logo:标题栏左上角logo
nav:右上角搜索后面的按钮
sidebar:左侧侧边栏
}

enhanceApp.js

在.vuepress文件夹下新增enhanceApp.js
该文件,可以添加对项目的额外处理
比如当前项目右上角三个按钮根据权限显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
export default ({
Vue, // VUE构造函数对象
options, // vue实例选项
router, // 路由对象
siteData//配置的数据
}) => {
async function getAuthorization(){
if(location.hostname.includes('localhost')){
return true
}
let cookie_str = document.cookie
let cookie_parts = cookie_str.split(';')
let auths = []
cookie_parts.forEach(c=>{
if (c){
let kv = c.split('=')
let k = kv[0].trim()
let v = kv[1].trim()
if (k == '_a'){
auths.push(v)
}
}
})
if(auths.length<1){
return false
}
for (var c of auths){
try {
let response = await axios.get(location.origin + '/api/v1/edit-user', {headers: {'X-Auth-Token': c}})
if(response.status < 400){
return true
}
} catch (err) {
console.log(err)
return false
}
}
}
getAuthorization().then(flag=>{
let divs = document.getElementsByClassName('nav-item')
if(flag){
divs[1].style.display = 'none'
divs[2].style.display = 'none'
}
})
}

更改页面呈现样式

增加悬浮锚点定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
page.vue
<template>
<main class="page">
<div v-show="$page.headers && isShowAncher && $page.frontmatter.ancherShow" class='ancherSection'>
<template v-for="(item,i) in headers">
<div :key="i">
<a @click="gotoAncher(item.slug)">{{item.title}}</a>
</div>
</template>
</div>
<slot name="top" />

<Content class="theme-default-content" />
<PageEdit />

<PageNav v-bind="{ sidebarItems }" />

<slot name="bottom" />
</main>
</template>

export default {
components: { PageEdit, PageNav},
props: ['sidebarItems'],
data () {
return {
isShowAncher: false, //滚动过程控制悬浮锚点
}
},
computed:{
headers:function(){//计算锚点内容
let ancherLevel = this.$page.frontmatter.ancherLevel
let sidebarDepth = this.$page.frontmatter.sidebarDepth?this.$page.frontmatter.sidebarDepth:this.$themeConfig.sidebarDepth
let levelStr = ancherLevel?ancherLevel:(sidebarDepth>1?'h2,h3':'h2')
if(ancherLevel && !(/^(h2|h3|h2\,\s*\h3)$/ig.test(ancherLevel.trim()))){
return []
}
if(levelStr.includes(',')){
return this.$page.headers && groupHeaders(this.$page.headers).length>0?this.$page.headers:[]
}else{
let level = +levelStr[1]
return this.$page.headers.filter(h => h.level === level)
}
}
},
updated(){
this.setPictureZoom()
},
mounted () {
let handleScroll= ()=>{
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop // 滚动条偏移量
this.isShowAncher = !!scrollTop
}
window.addEventListener('scroll',handleScroll,false)
this.setPictureZoom()
},
methods:{
//锚点定位
gotoAncher(ancher){
let el = document.getElementById(ancher)
let anchersHeight = document.getElementsByClassName('ancherSection')[0].clientHeight
window.scrollTo({
top: el.offsetTop-anchersHeight,
behavior: "smooth"
})
},
//添加图片放大功能
setPictureZoom () {
let img = document.querySelectorAll('p > img')
for (let i = 0; i < img.length; i++) {
if (img[i]) {
let a = document.createElement("a");
let parent = img[i].parentNode
parent.appendChild(a)
let src = img[i].getAttribute('src')
a.setAttribute('data-fancybox', '')
a.setAttribute('href', src)
a.appendChild(img[i])
}
}
}
}
}
</script>

<style lang="stylus">
@require '../styles/wrapper.styl'
.page
padding-bottom 2rem
display block
.ancherSection
position: fixed
background-color: white
box-shadow: rgb(234, 229, 229) 0px 1px 2px;
top: 64px
min-height: 42px
width: calc(100% - 320px);
display flex
align-items center
justify-content left
flex-wrap: wrap;
padding: 10px 0px 0px;
z-index:2
div
margin-left 20px
margin-bottom: 10px;
a
color black
cursor pointer

</style>

更换图标

在sidebarGroup.vue文件同级别新增arrow.svg图片,使用相对路径可插入

1
<img src='./arrow.svg' v-if="collapsable" :class="open ? 'down' : ''" style='width:20px' />

批量迁移

目录改造

原有系统页面图片按照产品名命名文件夹,故先将整体文件结构进行改造
原来目录结构

1
2
3
4
5
6
7
8
9
10
11
--content
--a
--a1.png
--a2.png
......省略若干
--a13.png
--b
--b1.png
--b2.png
......省略若干
--b13.png

改造成

1
2
3
4
5
6
7
8
9
10
11
12
13
--content
--a
--images
--a1.png
--a2.png
......省略若干
--a13.png
--b
--images
--b1.png
--b2.png
......省略若干
--b13.png

就是在当前父文件夹下新增images文件夹,然后把图片移进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var fs = require('fs');//引用文件系统模块
function readFileList(path, filesList) {
var files = fs.readdirSync(path);
files.forEach(function (itm) {
var obj = {};//定义一个对象存放文件的路径和名字
obj.path = path;//路径
obj.filename = itm//名字
filesList.push(obj);
})
}
var getFiles = {
//获取文件夹下的所有文件
getFileList: function (path) {
var filesList = [];
readFileList(path, filesList);
return filesList;
}
}
let files = getFiles.getFileList("./content/") //拿到a,b这一级别文件
for(let i=0;i<files.length;i++){
let filepath = files[i].path+files[i].filename+'/' //拼接a,b路径
let images = getFiles.getFileList(filepath) //拿到a,b下一级的文件,即所有图片
let imgpath = filepath+'images'
fs.mkdirSync(imgpath);//新建文件夹
for(let j = 0;j<images.length;j++){ //将图片移到新文件夹中
let oldpath = images[j].path+images[j].filename
let newpath = imgpath+'/'+images[j].filename
fs.rename(oldpath,newpath,function(err){
console.log(err)
})
}
}

转义文件

因为原项目为单页面文件,无法进行爬虫批量获取
最终实现也还是手动copy页面中dom,一个页面一个页面的copy
然后处理

安装转义包

1
npm install html-to-md

开始使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const html2md=require('html-to-md')
const fs = require('fs');
//在页面中找到使用说明的dom,copy之后存放到temp.txt中
//当做字符串被读取
var contentText = fs.readFileSync('temp.txt','utf-8');
let res = html2md(contentText)
//匹配md的‘[]()’书写形式
let reg1=/\[([\S ]*?)]\s?()\( *<?([^\s'"]*?(?:\([\S]*?\)[\S]*?)?)>?\s*(?:()(['"])(.*?)\5)? *\)/g
//匹配md的‘![]()’书写形式
let reg2 = /!\[([^\]]*?)][ \t]*()\([ \t]?<?([\S]+?(?:\([\S]*?\)[\S]*?)?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g
//在每篇开头注入转义规则front matter
res = '---\nsidebarDepth: 2 \nancherShow: true \nancherLevel: h3 \n--- \n' + res.replace(reg1,function(group,text){ //html-to-md包转义结果将标题转移成没有连接但有连接名的书写形式[xxx](),所以这里将其替换成 md写法
return text?'## '+text:group
}).replace(reg2,function(...res){ //将图片引用路径做统一处理
let arr = res[3].slice(res[3].lastIndexOf('/')+1).split('.')
return '![](./images/'+arr[0]+'.'+arr[2]+')'
})
//将处理后的文件写入相应的文件夹中的manual.md文件中,即生成md文件
fs.writeFile("./content/xxxx/manual.md", res, function(err) {
if(err) {
return console.log(err);
}
console.log("The file was saved!");
});

所以最终文件夹目录为

1
2
3
4
5
6
7
--content
--a
--images
--manual.md
--b
--images
--manual.md

现在将content目录下文件全都粘贴到docs文件夹下面,与.vuepres平级

1
2
3
4
5
6
7
8
--docs
--.vuepress
--a
--images
--manual.md
--b
--images
--manual.md

就可以在.vuepress文件夹下面的config.js中配置sidebar了

1
2
3
4
5
6
7
8
9
 sidebar: [
{
title: '产品a',
collapsable: true,
children:[
['/a/manual.md', 'a操作手册'],
]
}
}

小结

1.如何将公共开源的项目更契合的应用到公司级项目,对原项目的破坏是避免不了的,比如替换主题颜色,替换相关图标,以及为了新增一些原来项目中没有的功能,而去更改原有处理逻辑,所谓二次开发,实质上,可能应该叫破坏性开发,不遵循原来的使用规则,在原来基础上,造自己的规则

2.对于大批量重复性工作的处理,要学会使用工具,
比如局部大量替换,借助vscode的局部锁定(先选取范围再点击下图1,锁定按钮)
再比如规则类似的替换,又要善于利用vscode的正则匹配去替换,如下图2
vscode
如果属于大规模处理,还要学会自己造工具
比如上述批量处理目录结构,改造md文件
人之所以为人,学会使用工具才能算是进入了文明社会

My Little World

一个发版问题

发表于 2020-02-19

问题

用户正在使用某个页面期间,后台发布新的版本,用户再切换其他页面,
浏览器会报一个资源找不到的error,导致页面崩溃

原理

最终发布的版本会经过打包处理,每次打包的产物,文件名会不同
新发布的版本是最新打包的结果,上个版本发布的是上次发版前的结果
后台发布到服务器的项目新版本不再包含上个版本发布的文件
即服务器仅有新版本的文件
如果触发切换页面,此时浏览器向服务器发起请求上个版本的文件
服务器仅有最新版本文件,不再包含上个版本文件
故返回404,浏览器崩溃

背景

使用vue框架编写项目,使用vue-router进行路由切换

解决

利用路由

在路由生命钩子函数中添加onerror的处理,
让当前页面重新load一下,把资源拉回来,再让用户自己去点击要切换的页面

1
2
3
4
5
6
7
8
router.onError((error) => {
const pattern = /Loading chunk (\d)+ failed/g
const isChunkLoadFailed = error.message.match(pattern)
if (isChunkLoadFailed) {
$Message('系统更新,页面将进行刷新操作!')
window.location.reload()
}
})

轮询

使用webpack打包的时候生成一个hash.json的文件,然后前端轮询,
发现hash改变,就弹窗提示用户进行更新

借助CDN缓存策略

发版代码放到cdn上,用户未进行任何刷新操作则仍使用老代码,
用户进行了手动刷新,则拿到最新版本,进入新页面

1…789…27
YooHannah

YooHannah

264 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces