JSON是一种轻量级数据格式,可以方便地表示复杂数据结构。JSON对象有两个方法:stringify()和parse()。在简单的情况下,这两个方法分别可以将JavaScript序列化为JSON字符串,以及将JSON解析为原生JavaScript值。
JSON.stringify()
可以把一个JavaScript对象序列化为一个JSON字符串。
1 | let json1 = { |
默认情况下,JSON.stringify()会输出不包含空格或缩进的JSON字符串,因此jsonText的值是这样的:
1 | "{"title":"Json.stringify","author":["浪里行舟"],"year":2021}" |
在序列化JavaScript对象时,所有函数和原型成员都会有意地在结果中省略。此外,值为undefined
的任何属性也会被跳过。最终得到的就是所有实例属性均为有效JSON数据类型的表示。
在JSON.stringify()
方法一共能接受3个参数,其中两个可选的参数(分别是第二、第三个参数)。这两个可选参数可以用于指定其他序列化JavaScript对象的方式。第二个参数是过滤器,可以是数组或函数;第三个参数是用于缩进结果JSON字符串的选项。单独或组合使用这些参数可以更好地控制JSON序列化。
如果第二个参数是一个数组,那么JSON.stringify()
返回的结果只会包含该数组中列出的对象属性。比如下面的例子:
1 | let json1 = { |
在这个例子中,JSON.stringify()方法的第二个参数是一个包含一个字符串的数组:”weixin”。它对应着要序列化的对象中的属性,因此结果JSON字符串中只会包含这个属性:
1 | "{"weixin":"frontJS"}" |
如果第二个参数是一个函数,则行为又有不同。提供的函数接收两个参数:属性名(key)和属性值(value)。可以根据这个key决定要对相应属性执行什么操作。这个key始终是字符串,只是在值不属于某个键/值对时会是空字符串。
1 | const students = [ |
上面的代码,我们通过replacer将成绩从百分制替换为成绩等级。
1 | [ |
值得注意的是,如果stringify的第二个参数为函数那么它的返回值如果是undefined,那么对应的属性不会被序列化,如果返回其他的值,那么用返回的值替代原来的值进行序列化。
JSON.stringify()
方法的第三个参数控制缩进和空格。在这个参数是数值时,表示每一级缩进的空格数。例如,每级缩进4个空格,可以这样:
1 | let json1 = { |
这样得到的jsonText格式如下:
1 | { |
JSON.stringify()
在处理数据的时候同时考虑了数据转换和方便阅读,只不过,方便阅读这一点,常常被人忽略。
有时候,对象需要在JSON.stringify()
之上自定义JSON序列化。此时,可以在要序列化的对象中添加toJSON()
方法,序列化时会基于这个方法返回适当的JSON表示。
下面的对象为自定义序列化而添加了一个toJSON()方法:
1 | let json1 = { |
注意,箭头函数不能用来定义toJSON()方法。主要原因是箭头函数的词法作用域是全局作用域,在这种情况下不合适。
1 | //判断数组是否包含某对象 |
我们还可以使用JSON.stringify()
方法,来判断两个对象是否相等。
1 | // 判断对象是否相等 |
不过这种方式存在着较大的局限性,对象如果调整了键的顺序,就会判断出错!
1 | // 调整对象键的位置后 |
localStorage/sessionStorage
默认只能存储字符串,而实际开发中,我们往往需要存储对象类型,那么此时我们需要在存储时利用json.stringify()
将对象转为字符串,在取本地缓存时,使用json.parse()
转回对象即可。
1 | // 存数据 |
开发中,有时候怕影响原数据,我们常深拷贝出一份数据做任意操作,使用JSON.stringify()
与JSON.parse()
来实现深拷贝是很不错的选择。
1 | let arr1 = [1, 3, { |
这是利用JSON.stringify
将对象转成JSON字符串,再用JSON.parse
把字符串解析成对象,一去一来,新的对象产生了,新对象会开辟新的栈,实现深拷贝。
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify
和JSON.parse
处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null
)了。
1 | let arr1 = [1, 3, function () { }, { |
JSON.stringify()
虽然功能很强大,但是有些属性无法被stringify,所以在开发中需注意以下几种情况,以免产生一些始料未及的BUG。
1 | let myObj = { |
分为两种情况:
undefined
、任意的函数以及symbol
值在序列化的过程中会被转换成 null
1 | JSON.stringify([undefined, function () { }, Symbol("")]); |
1 | JSON.stringify({ x: undefined, y: function () { }, z: Symbol("") }); |
如果一个对象的属性值通过某种间接的方式指回该对象本身,那么就是一个循环引用。比如:
1 | let bar = { |
这种情况下,序列化会报错的:
1 | // 错误信息 |
不可枚举的属性默认会被忽略:
1 | let personObj = Object.create(null, { |
JSON.stringify()
用于将JavaScript对象序列化为JSON字符串,这方法有一些选项可以用来改变默认的行为,以实现过滤或修改流程。不过也应该注意有些属性是无法被 stringify,所以开发时候应该避开这些坑!
]]>
在自身不确定服务器是否开启了支持gzip时可以通过以下方法查看。
方法一
打开Chrome DevTools
,选中Network
面板右键点击表头
如果Content-Encoding
中有gzip
值则代表已经开启支持gzip。
方法二
打开网址http://tool.chinaz.com/Gzips进行查询,如:
]]>在 Promise 出现以前,在我们处理多个异步请求嵌套时,代码往往是个回掉地狱
1 | let fs = require('fs') |
为了拿到回调的结果,我们必须一层一层的嵌套,可以说是相当恶心了。而且基本上我们还要对每次请求的结果进行一系列的处理,使得代码变的更加难以阅读和难以维护,这就是传说中臭名昭著的回调地狱~产生回调地狱的原因归结起来有两点:
原因分析出来后,那么问题的解决思路就很清晰了:
Promise 正是用一种更加友好的代码组织方式,解决了异步嵌套的问题。
我们来看看上面的例子用 Promise 实现是什么样的:
1 | let fs = require('fs') |
在传统的异步编程中,如果异步之间存在依赖关系,就需要通过层层嵌套回调的方式满足这种依赖,如果嵌套层数过多,可读性和可以维护性都会变得很差,产生所谓的“回调地狱”,而 Promise 将嵌套调用改为链式调用,增加了可阅读性和可维护性。也就是说,Promise 解决的是异步编码风格的问题。 那 Promise 的业界实现都有哪些呢? 业界比较著名的实现 Promise 的类库有 bluebird、Q、ES6-Promise。
我们想要手写一个 Promise,就要遵循 Promise/A+ 规范,业界所有 Promise 的类库都遵循这个规范。
其实 Promise/A+ 规范对如何实现一个符合标准的 Promise 类库已经阐述的很详细了。每一行代码在 Promise/A+ 规范中都有迹可循,所以在下面的实现的过程中,我会尽可能的将代码和 Promise/A+ 规范一一对应起来。
中文译文:https://www.ituring.com.cn/article/66566
本文讲解使用ES5方式实现,代码仓库中补充ES6 class方式实现
结合Promise/A+的规范,边分析基础特征,边一步步实现代码:
pending
,fulfilled
,or rejected
根据三种状态,我们先定义成枚举,方便后续使用
1 | const State = { |
当处于某一种状态时又需要满足以下的条件:
根据以上条件当改变状态时,抽象成一个通用改变State的方法:
1 | /** |
then
方法的对象或函数,且new Promise()
时,需要传递一个executor()
函数执行器。那么我们先定义两个工具函数,后续也会用到
1 | // 检测是否是一个函数 |
那么根据定义可以实现一下代码
1 | /** |
executor
并接受两个参数,分别是fulfill
和reject
,且该函数执行器立即执行。并且这里要结合第一点:
Promise
中有变量来代表当前的状态,且状态默认为Pending
Promise
有一个value
保存成功状态的值,可以是undefined/thenable/promise
;「规范 Promise/A+ 1.3」
Promise
有一个reason
保存失败状态的值;「规范 Promise/A+ 1.5」
1 | // 调用方法时改变Promise状态且拥有一个终值 |
通常fulfill的定义也可以表示为resolve,但是本文和后续方法名字有点冲突不方便查看所以定义为fulfill,平常所用的resolve意义一样。
then
方法以访问其当前值、终值和据因。且接受两个参数onFulfilled
, onRejected
onFulfilled
和 onRejected
都是可选参数,并且两者不为函数则都需要忽略(内部实现用默认函数替代)
如果 onFulfilled
是函数:
promise
执行结束后其必须被调用,其第一个参数为 promise
的终值promise
执行结束前其不可被调用如果 onRejected
是函数:
promise
被拒绝执行后其必须被调用,其第一个参数为 promise
的据因promise
被拒绝执行前其不可被调用onFulfilled
和 onRejected
只有在执行环境堆栈仅包含平台代码时才可被调用(后面再解释)
根据以上定义,完善现在的代码
1 | // 增加try catch保证稳定执行,因为changeState在不正当切换State时会抛出错误 |
then
方法必须返回一个 promise
对象Promise的优势就在于链式调用,在我们使用Promise的时候,当then函数中返回了一个值,不管是什么值,我们都能在下一个then中获取到,这就是所谓的then的链式调用。而且,当我们不再then
中放入参数,例如:promise.then().then()
,那么其后面的then
依然可以得到之前then
返回的值,这就是值得穿透。这也是规范实现得思路。
1 | const promise2 = promise1.then(onFulfilled, onRejected); |
onFulfilled
或者 onRejected
返回一个值 x
,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x)
onFulfilled
或者 onRejected
抛出一个异常 e
,则 promise2
必须拒绝执行,并返回拒因 e
onFulfilled
不是函数且 promise1
成功执行, promise2
必须成功执行并返回相同的值onRejected
不是函数且 promise1
拒绝执行, promise2
必须拒绝执行并返回相同的据因理解上面的“返回”部分非常重要,即:不论
promise1
被reject
还是被resolve
时promise2
都会被resolve
,只有出现异常时才会被rejected
。
分析一下上述可以看到,then方法需要返回一个新的Promise对象(promise2),且需要进行处理promise1中then
两个参数的返回值
并且多了一个resolve
方法定义Promise解决过程
,方法的参数传入(promise2,x)
。具体下面再实现,先进行调用。
1 | /** |
我们先看看规范是怎么定义的:
Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为[[Resolve]](promise, x)
,如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise 。
这种
thenable
的特性使得 Promise 的实现更具有通用性:只要其暴露出一个遵循 Promise/A+ 协议的 then 方法即可;这同时也使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。
运行 [[Resolve]](promise, x)
需遵循以下步骤:
x 与 promise 相等
如果 promise 和 x 指向同一对象,以 TypeError
为据因拒绝执行 promise
x 为 Promise时 ,则使 promise 接受 x 的状态 :
如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝
如果 x 处于执行态,用相同的值执行 promise
如果 x 处于拒绝态,用相同的据因拒绝 promise
x 为对象或函数时
把 x.then
赋值给 then
如果取 x.then
的值时抛出错误 e ,则以 e 为据因拒绝 promise
如果 then 是函数,将 x 作为函数的作用域this
调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise
,第二个参数叫做 rejectPromise
:
(1) 如果 resolvePromise
以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
(2) 如果 rejectPromise
以据因 r 为参数被调用,则以据因 r 拒绝 promise
(3) 如果 resolvePromise
和 rejectPromise
均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
(4) 如果调用 then 方法抛出了异常 e, 如果resolvePromise
或 rejectPromise
已经被调用,则忽略之。否则以 e 为据因拒绝 promise
(5) 如果 then 不是函数,以 x 为参数执行 promise
如果 x 不为对象或者函数,以 x 为参数执行 promise
注:如果一个 promise 被一个循环的
thenable
链中的对象解决,而[[Resolve]](promise, thenable)
的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的TypeError
为据因来拒绝 promise。
那么根据定义我们来完善一下先前空实现的resolve
方法:
1 | /** |
看到这里问题来了,Promise的异步概念还没有体现到,这是因为规范后面有一个注释:
有英文能力的可以看一下原文:https://promisesaplus.com/#notes
注释
在本文第四点提到的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保
onFulfilled
和onRejected
方法异步执行,且应该在then
方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。由于 promise 的实施代码本身就是平台代码(译者注:即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。译者注:这里提及了 macrotask 和 microtask 两个概念,这表示异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。
两个类别的具体分类如下:
- macro-task: script(整体代码),
setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
- micro-task:
process.nextTick
,Promises
(这里指浏览器实现的原生 Promise),Object.observe
,MutationObserver
详见 stackoverflow 解答 或 这篇博客
最终完善一下then
方法,直接贴所有代码
1 | /** |
需要安装mocha
以及promises-aplus-tests
库
1 | $ npm i -D mocha promises-aplus-tests |
新建test.js文件,并添加以下代码:
1 | var Promise = require('./promise-step'); |
手动执行命令
1 | $ ./node_modules/mocha/bin/mocha |
或者在package.json
中添加scripts
1 | "scripts": { |
部分运行结果如下
1 | The value is `1` with `Number.prototype` modified to have a `then` method |
我们来看看MDN上的定义:Promise.resolve()
**Promise.resolve(value)**
方法返回一个以给定值解析后的Promise
对象。如果这个值是一个 promise ,那么将返回这个 promise如果这个值是thenable(即带有"then"
方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。
根据定义我们来手写实现一下
1 | Promise.resolve = function (value) { |
测试一下:
1 | Promise.resolve(new Promise((resolve, reject) => { |
控制台等待 3s
后输出:
1 | "ok success" |
Promise.reject()
方法返回一个带有拒绝原因的Promise
对象。
1 | Promise.reject = function (reason) { |
catch() 方法返回一个Promise (en-US),并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected)
相同。
1 | Promise.prototype.catch = function (callback) { |
finally()
方法返回一个Promise
。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise
是否成功完成后都需要执行的代码提供了一种方式。
这避免了同样的语句需要在then()
和catch()
中各写一次的情况。
注意: 在
finally
回调中throw
(或返回被拒绝的promise)将以throw()
指定的原因拒绝新的promise.
1 | Promise.prototype.finally = function (callback) { |
Promise.all() 方法接收一个promise的iterable类型(注:Array,Map,Set都属于ES6的iterable类型)的输入,并且只返回一个Promise
实例, 那个输入的所有promise的resolve回调的结果是一个数组。这个Promise
的resolve回调执行是在所有输入的promise的resolve回调都结束,或者输入的iterable里没有promise了的时候。它的reject回调执行是,只要任何一个输入的promise的reject回调执行或者输入不合法的promise就会立即抛出错误,并且reject的是第一个抛出的错误信息。
1 | Promise.all = function(arr) { |
测试一下
1 | const promise1 = Promise.resolve(3); |
从 promise 的使用方法入手,构造出了 promise 的大致框架,然后根据 promise/A+ 规范填充代码,重点实现了 then 的链式调用和值的穿透;然后使用测试脚本对所写的代码是否符合规范进行了测试;最后完成了 Promise 的 API 的实现。弄懂 promise 其实并不复杂,归根结底还是孰能生巧。
且涉及到了JS 的循环机制EventLoop(主线程、微任务、渲染、宏任务)。
]]>一套可维护的CSS库离不开一套好的CSS设计模式或者架构。那么这三个主流的CSS设计思想和一个最近通用的CSS设计思想:OOCSS、SMACSS、BEMCSS、METACSS都是必须要去了解的。
OOCSS
,字面意思是面向对象的CSS,是由Nicole Sullivan
提出的css理论,虽说是理论,实则更像一种程序员间约定的规范:
在 OOCSS
的观念中,强调重复使用 class
,而应该避免使用 id
作为 CSS 的选择器。OOCSS追求元件的复用,其class
命名更为抽象,一般不体现具体事物,而注重表现层的抽取。
SMACSS
通过一个灵活的思维过程来检查你的设计过程和方式是否符合你的架构
设计的主要规范有三点:
这一点是SMACSS
的核心。SMACSS认为css有5个类别,分别是:
基础规范,描述的是任何场合下,页面元素的默认外观。它的定义不会用到class和ID。css reset
也属于此类。常见的如normalize.css
, CSS Tools
布局规范,元素是有层次级别之分的,Layout Rules属于较高的一层,它可以作为层级较低的Module Rules元素的容器。左右分栏、栅格系统等都属于布局规范。布局是一个网站的基本,无论是左右还是居中,甚至其他什么布局,要实现页面的基本浏览功能,布局必不可少。SMACSS还约定了一个前缀l-/layout-来标识布局的class。举个最普遍的例子。
1 | .layout-header {} |
模块规范,模块是SMACSS最基本的思想,同时也是大部分CSS理论的基本,将样式模块化就能达到复用和可维护的目的,但是SMACSS提出了更具体的模块化方案。SMACSS中的模块具有自己的一个命名,隶属于模块下的类皆以该模块为前缀,例子如下:
1 | .todolist{} |
可以看到todolist
作为一个模块,包含了title,image,article等组件,同时还可以加上如todolist-background-danger
等修饰类,在模块内可以使用其名称做前缀任意组织模块结构,但目的是让其变得更易用,提高可扩展性和灵活度,如果只是为了修饰而修饰,写出大量没有任何复用性的类,便是一种弄巧成拙的做法。
状态规范,这个应该很多前端开发者都很好理解,描述的是任一元素在特定状态下的外观。例如,一个消息框可能有success和error等状态。与OOCSS抽取修饰类的方式的不同,SMACSS是抽取更高级别的样式类,得到更强的复用性,如隐藏某个元素的写法:
1 | .is-hidden{ |
主题规范,描述了页面主题外观,一般是指颜色、背景图。Theme Rules可以修改前面4个类别的样式,且应和前面4个类别分离开来(便于切换,也就是“换肤”)。SMACSS的Theme Rules不要求使用单独的class命名,也就是说,你可以在Module Rules中定义.header{ }然后在Theme Rules中也用.header { }来定义需要修改的部分(后加载覆盖前加载样式内容)
命名规范
按照前面5种的划分:
.l-header
、.l-sidebar
。.media
、.media-image
。.is-active
、.is-hidden
。.theme-a-background
、.theme-a-shadow
。最小适配深度原则,简单的例子:
1 | /* depth 1 */ |
两段css的区别在于html和css的耦合度(这一点上和OOCSS的分离容器和内容的原则不谋而合)。可以想到,由于上面的样式规则使用了继承选择符,因此对于html的结构实际是有一定依赖的。如果html发生重构,就有可能不再具有这些样式。对应的,下面的样式规则只有一个选择符,因此不依赖于特定html结构,只要为元素添加class,就可以获得对应样式。
当然,继承选择符是有用的,它可以减少因相同命名引发的样式冲突(常发生于多人协作开发)。但是,我们不应过度使用,在不造成样式冲突的允许范围之内,尽可能使用短的、不限定html结构的选择符。这就是SMACSS的最小化适配深度的意义。
BEM 分别代表着:Block(块)、Element(元素/子块/组成部分)、Modifier(修饰符),是一种组件化的 CSS 命名方法和规范,由俄罗斯 Yandex 团队所提出。其目的是将用户界面划分成独立的(模)块,使开发更为简单和快速,利于团队协作开发。
特点
组件化/模块化的开发思路。书写方式解耦化,不会造成命名空间的污染,如:.xxx ul li
写法带来的潜在嵌套风险。命名方式化扁平,避免样式层级过多而导致的解析效率降低,渲染开销变大。组件结构独立化,减少样式冲突,可以将已开完成的组件快速应用到新项目中。有着较好的维护性、易读性、灵活性。规则
BEM的命名模式在社区中有着不同方式,以下为 Yandex 团队所提出的命名规则为:
.[Block 块]__[Element 元素]_[Modifier 修饰符]
不同的命名模式,区别在于BEM之间的连接符号不同,依个人而定:
.[Block 块]__[Element 元素]--[Modifier 修饰符]
任何一种规范,都是基于实际需求而定,便于团队开发和维护扩展,每个规范都是经过合理评估后所得出的一种“思路”和“建议”。
是一个独立的实体,即通常所说的模块或组件。
例:header、menu、search
规则:块名需能清晰的表达出,其用途、功能或意义,具有唯一性。块名称之间用-连接。每个块名前应增加一个前缀,这前缀在 CSS 中有命名空间(如:m-、u-、分别代表:mod 模块、ui 元件)。每个块在逻辑上和功能上都相互独立。由于块是独立的,可以在应用开发中进行复用,从而降低代码重复并提高开发效率。块可以放置在页面上的任何位置,也可以互相嵌套。同类型的块,在显示上可能会有一定的差异,所以不要定义过多的外观显示样式,主要负责结构的呈现。
这样就能确保块在不同地方复用和嵌套时,增加其扩展性。综上所述,最终我们可以把BEM规则最终定义成:
.[命名空间]-[组件名/块]__[元素名/元素]--[修饰符]
情景 需要构建一个 search
组件。
写法 .m-search{}
结构
如果打算开发一套框架,可以使用具有代表性的缩写,用来表示命名空间:Element UI(el-)
、Ant Design(ant-)
、iView(ivu-)
。
是块中的组成部分,对应块中的子元素/子节点。
例:header title、menu item、list item
规则:元素名需能简单的描述出,其结构、布局或意义,并且在语义上与块相关联。块与元素之间用__
连接。不能与块分开单独使用。块的内部元素,都被认为是块的子元素。一个块中元素的类名必须用父级块的名称作为前缀,因此不能写成:block__elem1__elem2
。情景 search 组件中包含 input 和 button,是列表中的一个子元素。
写法 .m-search{}
、.m-search__input{}
、.m-search__button{}
结构
1 | <!-- search 组件 --> |
原则上书写时不会出现两层以上的嵌套,所有样式都为平级,嵌套只出现在.m-block_active
,状态激活时的情况。
定义块和元素的外观、状态或类型。
例:color
、disabled
、size
规则:修饰符需能直观易懂表达出,其外观、状态或行为。修饰符用_连接块与元素。修饰符不能单独使用。在必要时可进行扩展,书写成:block__elem_modifier_modifier
,第一个modifier表示其命名空间。情景 假定 search 组件有多种外观,我们选择其中一种。并且在用户未输入内容时,button 显示为禁用样式。
写法.m-search{}
、 .m-search_dark{}
、.m-search__input{}
、 .m-search__button{}
、 .m-search__button_disabled{}
结构
1 | <!-- dark 表明 search 组件的外观 --> |
很多人觉得 BEM 写法难看,审美本是“智者见智,仁者见仁”的事。刚刚接触可能是会觉得有点奇怪,但所有东西都有一个适应过程。如果仅仅为了好看,规避其优点,我认为得不偿失。个人建议可以尝试使用 BEM 规范来书写代码。
BEM 命名会使得 Class 类名变长,但经过 GZIP 等压缩后,文件的体积其实并无太大影响。
就和早年提出 CSS语义化 一样,不要为了语义而去语义,语义化本身的作用就是帮助大家更好的识别代码,所有的规范都是基于项目的发展和团队的协作,团队可以根据成员的意愿选择最合适的方式。
一些写在全局的通用方法,是SMACSS中通用方法思想的分支,一般以css属性、Emmet css缩写或功能来命名,通常以一个css属性为一个单位
表示属性的:
1 | .df { display: flex; } |
表示功能的:
1 | .tcut { |
以此类推,封装好放到全局来使用,快速添加属性来开发页面。
smacss覆盖了所有的细节点;bemcss着重css的命名和语义化;oocss着重可复用,把每一个dom节点当成一个对象,是css返璞归真的思想;metacss着重快速开发快速添加属性,颗粒度更细,通过在html代码中添加类名来添加属性,不必再去找相对应的选择器中的css代码来修改样式。
]]>
产生该问题的原因是由于Windows平台和linux平台的默认换行符是不一样的,linux使用的是0x0A(LF)
而Windows使用的是0x0D0A(CRLF)
,这就导致了当Windows下的代码放到linux下运行时,虽然代码没有错,但是linux下的git检测到项目的换行符为CRLF时会自动换成LF。
出现这个问题的症状表现为git会提示项目的每一个文件的所有位置都发生了修改,但是查看diff的时候发现其实哪都没修改,这是因为换行符被换了但是我们是看不出来的。
由于代码多是运行在linux,所以现在主流的换行符标准就是LF,所以我们的项目一开始就应该有将换行符设置为LF的意识.
项目一开始创建,还没有加入git仓库的时候就应该将换行符设置为LF,vscode等工具都提供了这个简单的功能,
如果项目已经加入了git仓库,那就让git帮我们解决问题,git有一个autocrlf
配置,可以在我们提交时自动转换换行符,它有3个选项:
另一个设置项safecrlf
用于检查文件是否包含着混合换行符,也有3个选项:
所以,如果我们要将已经加入git的大量CRLF结尾文件批量转换成LF结尾的文件,可以这样设置:
1 | $ git config --global core.autocrlf input |
这样设置之后,先将项目提交一次,这样所有的文件就都会被改成LF结尾.
MD5算法是典型的消息摘要算法,其前身有MD2、MD3和MD4算法,它由MD4、MD3和MD2算法改进而来。不论是哪一种MD算法,它们都需 要获得一个随机长度的信息并产生一个128位的信息摘要。如果将这个128位的二进制摘要信息换算成十六进制,可以得到一个32位的字符串,故我们见到的 大部分MD5算法的数字指纹都是32为十六进制的字符串。
1989年,著名的非对称算法RSA发明人之一—-麻省理工学院教授罗纳德.李维斯特开发了MD2算法。这个算法首先对信息进行数据补位,使信 息的字节长度是16的倍数。再以一个16位的检验和做为补充信息追加到原信息的末尾。最后根据这个新产生的信息计算出一个128位的散列值,MD2算法由 此诞生。
1990年,罗纳德.李维斯特教授开发出较之MD2算法有着更高安全性的MD4算法。在这个算法中,我们仍需对信息进行数据补位。不同的是,这种补 位使其信息的字节长度加上448个字节后成为512的倍数(信息字节长度mod 512 =448)。此外,关于MD4算的处理和MD2算法有很大的差别。但最终仍旧会获得一个128为的散列值。MD4算法对后续消息摘要算法起到了推动作用, 许多比较有名的消息摘要算法都是在MD4算法的基础上发展而来的,如MD5、SHA-1、RIPE-MD和HAVAL算法等。
1991年,继MD4算法后,罗纳德.李维斯特教授开发了MD5算法,将MD算法推向成熟。MD5算法经MD2、MD3和MD4算法发展而来,算法复杂程度和安全强度打打提高,但浙西MD算法的最终结果都是产生一个128位的信息摘要。这也是MD系列算法的特点。MD5算法的算法特点如下: (1)压缩性:任意长度的数据,算出的MD5值长度都是固定的。 (2)容易计算:从原数据计算出MD5值很容易。 (3)抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。 (4)弱抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。 (5)强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
在破解md5方面,最常用的方法是“跑字典”,有两种方法得到字典,一种是日常搜集的用做密码的字符串表,另一种是用排列组合方法生成的,先用MD5程序计算出这些字典项的MD5值,然后再用目标的MD5值在这个字典中检索。我们假设密码的最大长度为8位字节(8 Bytes),同时密码只能是字母和数字,共26+26+10=62个字节,排列组合出的字典的项数则是P(62,1)+P(62,2)….+P(62,8),那也已经是一个很天文的数字了,存储这个字典就需要TB级的磁盘阵列,而且这种方法还有一个前提,就是能获得目标账户的密码MD5值的情况下才可以。
所以总体而言,md5加密是十分安全的,即使有一些瑕疵,但并不影响具体的使用,外加md5是免费的,所以它的应用还是十分广泛的。
MD5算法,可以用来保存用户的密码信息。为了更好的保存,可以在保存的过程中,加入盐。/在保存用户密码的时候,盐可以利用生成的随机数。可以将密码结合MD5加盐,生成的数据摘要和盐保存起来 。以便于下次用户验证使用。在用户表里面,也保存salt。
每个文件都可以用MD5验证程序算出一个固定的MD5值,是独一无二的。一般来说,开发方会在软件发布时预先算出文件的MD5值,如果文件被盗用,加了木马或者被篡改版权,那么它的MD5值也随之改变,也就是说我们对比文件当前的MD5值和它标准的MD5值来检验它是否正确和完整。 (1)例如网盘中的秒传4G文件,可以使用用户需要上传的文件进行Md5运算,判断与服务器中是否存在该文件,如果存在只需添加文件索引,不存在再真正上传。 (2)例如自动升级的客户端,判断下载的程序安装包是否完整,可以计算文件的MD5值,与服务器端计算的Md5值进行比对。
我们知道,如果直接对密码进行散列,那么黑客可以对通过获得这个密码散列值,然后通过查散列值字典(例如MD5密码破解网站),得到某用户的密码。
加Salt可以一定程度上解决这一问题。所谓加Salt方法,就是加点“佐料”。其基本想法是这样的:当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后散列,再比较散列值,已确定密码是否正确。
这里的“佐料”被称作“Salt值”,这个值是由系统随机生成的,并且只有系统知道。这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不同,他们的散列值也是不同的。即便黑客可以通过自己的密码和自己生成的散列值来找具有特定密码的用户,但这个几率太小了(密码和salt值都得和黑客使用的一样才行)。
下面详细介绍一下加Salt散列的过程。介绍之前先强调一点,前面说过,验证密码时要使用和最初散列密码时使用“相同的”佐料。所以Salt值是要存放在数据库里的。
用户注册时,
用户登录时,
有时候,为了减轻开发压力,程序员会统一使用一个salt值(储存在某个地方),而不是每个用户都生成私有的salt值。
例子详解:
早期的软件系统或者互联网应用,数据库中设计用户表的时候,大致是这样的结构:
数据存储形式如下:
主要的关键字段就是这么两个,一个是登陆时的用户名,对应的一个密码,而且那个时候的用户名是明文存储的,如果你登陆时用户名是 123,那么数据库里存的就是 123
。这种设计思路非常简单,但是缺陷也非常明显,数据库一旦泄露,那么所有用户名和密码都会泄露,后果非常严重。
为了规避第一代密码设计的缺陷,聪明的人在数据库中不在存储明文密码,转而存储加密后的密码,典型的加密算法是 MD5 和 SHA1,其数据表大致是这样设计的:
数据存储形式如下:
假如你设置的密码是123
,那么数据库中存储的就是202cb962ac59075b964b07152d234b70
或 40bd001563085fc35165329ea1ff5c5ecbdbbeef
。当用户登陆的时候,会把用户输入的密码执行 MD5(或者 SHA1)后再和数据库就行对比,判断用户身份是否合法,这种加密算法称为散列。
严格地说,这种算法不能算是加密,因为理论上来说,它不能被解密。所以即使数据库丢失了,但是由于数据库里的密码都是密文,根本无法判断用户的原始密码,所以后果也不算太严重。
本来第二代密码设计方法已经很不错了,只要你密码设置得稍微复杂一点,就几乎没有被破解的可能性。但是如果你的密码设置得不够复杂,被破解出来的可能性还是比较大的。
好事者收集常用的密码,然后对他们执行 MD5 或者 SHA1,然后做成一个数据量非常庞大的数据字典,然后对泄露的数据库中的密码就行对比,如果你的原始密码很不幸的被包含在这个数据字典中,那么花不了多长时间就能把你的原始密码匹配出来。这个数据字典很容易收集,CSDN 泄露的那 600w 个密码,就是很好的原始素材。
于是,第三代密码设计方法诞生,用户表中多了一个字段:
数据存储形式如下:
Salt 可以是任意字母、数字、或是字母或数字的组合,但必须是随机产生的,每个用户的 Salt 都不一样,用户注册的时候,数据库中存入的不是明文密码,也不是简单的对明文密码进行散列,而是 MD5( 明文密码 + Salt),也就是说:
1 | MD5('123' + '1ck12b13k1jmjxrg1h0129h2lj') = '6c22ef52be70e11b6f3bcf0f672c96ce' |
由于加了 Salt,即便数据库泄露了,但是由于密码都是加了 Salt 之后的散列,坏人们的数据字典已经无法直接匹配,明文密码被破解出来的概率也大大降低。
是不是加了 Salt 之后就绝对安全了呢?淡然没有!坏人们还是可以他们数据字典中的密码,加上我们泄露数据库中的 Salt,然后散列,然后再匹配。但是由于我们的 Salt 是随机产生的,假如我们的用户数据表中有 30w 条数据,数据字典中有 600w 条数据,坏人们如果想要完全覆盖的坏,他们加上 Salt 后再散列的数据字典数据量就应该是 300000* 6000000 = 1800000000000,一万八千亿啊,干坏事的成本太高了吧。但是如果只是想破解某个用户的密码的话,只需为这 600w 条数据加上 Salt,然后散列匹配。可见 Salt 虽然大大提高了安全系数,但也并非绝对安全。
实际项目中,Salt 不一定要加在最前面或最后面,也可以插在中间嘛,也可以分开插入,也可以倒序,程序设计时可以灵活调整,都可以使破解的难度指数级增长。
该文以腾讯云创建的Server 2016服务器为例
需要先放通服务器安全组端口,以腾讯云服务器为例:
以及关闭服务器的防火墙。
连接远程服务器,打开服务器管理器。
点击添加角色和功能
,进行下图所示配置。
开始之前直接点击下一步
安装类型直接点击下一步
服务器选择直接点击下一步
服务器角色增加网络策略和访问服务
以及远程访问
选择添加功能
勾选后点击下一步
功能默认即可,直接点击下一步
网络策略和访问服务直接点击下一步
远程访问直接点击下一步
角色服务增加DirectAccess和VPN(RAS)
以及路由
选择添加功能即可
勾选后点击下一步
Web服务器角色直接点击下一步
角色服务默认即可直接点击下一步
确认安装
点击安装后等待安装完毕关闭即可。
打开服务器管理器,点击工具,选择路由和远程访问
右键本地服务器,选择“配置并启用路由和远程访问”,启动配置向导。
点击下一步
选择自定义配置
全部勾选,并点击下一步
点击完成
如出现该提示直接点击确定
点击启动服务
展开本地服务器,展开IPv4
,右键NAT
,选择新建接口
选择以太网
,点击确定
勾选公用以及启用NAT,如图所示,点击确定
再次展开本地服务器,展开IPv4
,右键NAT
,选择新建接口
选择内部
,点击确定
勾选专用,点击确定
配置本地服务器属性
点击IPv4
标签页为远端连接分配IP地址池
再次配置本地服务器属性
选择安全
标签页,配置允许L2TP策略选项,并填入预共享密钥,点击确定
务必记住该密钥,后续需要填写
点击确定
打开服务器管理器,点击工具选择计算机管理
展开本地用户和组,选择新建组
展开本地用户和组,选择新用户
点击新建的用户名右键选择属性
按图中依次点击隶属于
–添加
–高级
—立即查找
—VPNGroup(刚刚创建的组)
—确定
点击确定
配置 VPN 访问权限,回到服务器管理器,点击NAPS
–选择服务器
–启动网络策略服务器
配置
展开策略,右键点击网络策略并新建
填写名称并在“网络访问服务器的类型”中选择远程访问服务器(VPN 拨号)
,点击下一步
在指定条件中,根据实际需求,选择合适的匹配条件。比如,文中选择了域中的VPNGroup
用户组。
点击下一步
点击下一步
点击下一步
点击下一步
点击完成
然后重启服务器(重启服务)但懒得找服务直接重启服务器了。
重启完成后打开服务器管理器,找到远程访问,并启动对应的服务。
右键启动即可
打开设置
—网络
,点击+
号新建
选择图中对应类型
打开设置
—网络和Internet
—VPN
配置如图所示
这边测试在Win7或Win10连接时会出现无法建立计算机与VPN服务器之间的网络连接,因为远程服务器未响应
的问题。查找了文章修复了问题。
解决办法
按windows图标键 + R键
>在运行中输入regedit
,单击确定
,进入注册表编辑器
在注册表编辑器”页面的左侧导航树点开 HKEY_LOCAL_MACHINE
>SYSTEM
>CurrentControlSet
>Services
>PolicyAgent
在右边空白处新建 > DWORD值
,名称为AssumeUDPEncapsulationContextOnSendRule
右键单击AssumeUDPEncapsulationContextOnSendRule
,选择“修改”,进入修改界面,修改值为2
(表示可以与位于NAT设备后方的服务器建立安全关联)
重启电脑即可。一般Win7配置完该步骤后即可连接,但Win10还会出现问题。
解决办法
打开更改适配器选项,找到对应的VPN名称的适配器,右键属性
打开安全选项,选择使用这些协议勾上;注意此处还有高级设置里面的L2TP身份验证类型,这里也要填写的(秘钥方式还是证书方式)
再次连接,即可修复
新建一个.bat
文件,如修复注册表.bat
,右键编辑增加以下代码:
1 | @echo off |
保存后双击运行即可。
纯前端利用Canvas来进行图片压缩的方法
1 | // 压缩方法 |
使用案例
1 | <input id="upload" type="file" /> |
1 | // 支持的类型 |
]]>
1 | > Task :compileKotlin FAILED |
在混编开发过程中,打包的时候出现了该问题,排查很久,总算找到了原因:没有配置JAVA_HOME环境变量。
解决方案
安装好JDK后,获取JAVA的安装路径
1 | $ /usr/libexec/java_home -V |
mac下编辑profile:
1 | $ vi ~/.bash_profile |
编辑完成后,按esc
退出插入模式,输入:wq
退出保存即可。
1 | $ source ~/.bash_profile |
生效后验证:
1 | $ echo $JAVA_HOME |
问题解决。
]]>目前移动端的设备已经非常多,并且不同的设备手机屏幕也不相同。
目前做移动端开发都要针对不同的设备进行一定的适配,无论是移动原生开发、小程序、H5页面。
在进行Flutter开发时,我们通常不需要传入尺寸的单位,那么Flutter使用的是什么单位呢?
在Flutter开发中,我们使用的是对应的逻辑分辨率
获取屏幕上的一些信息,可以通过MediaQuery:
1 | // 1.媒体查询信息 |
获取一些设备相关的信息,可以使用官方提供的一个库:
1 | dependencies: |
假如我们有下面这样一段代码:
1 | class HomePage extends StatelessWidget { |
上面的代码在不同屏幕上会有不同的表现:
在前端开发中,针对不同的屏幕常见的适配方案有下面几种:
该文采用类似小程序的rpx方案来完成Flutter的适配
小程序中rpx的原理是什么呢?
那么我们就可以通过上面的计算方式,算出一个rpx,再将自己的size和rpx单位相乘即可:
我们自己来封装一个工具类:
1 | import 'package:flutter/material.dart'; |
初始化ScreenAdapterUtils
类的属性:
MaterialApp
的Widget中使用context,否则是无效的1 | class HomePage extends StatelessWidget { |
使用rpx来完成屏幕适配:
1 | class HomePage extends StatelessWidget { |
实现效果:
如果每次我们需要将现在的宽度或者高度,去使用ScreenAdapterUtils.instance().px(200)
或者ScreenAdapterUtils.instance().rpx(400)
类似的方式去适配,显然看起来非常麻烦。
有没有更好的方案可以实现了?比如 200.px或者400.rpx,非常的清晰简洁
当然可以,我们需要依赖Dart语言的一个特性:extension
2.7.0
开始,可以通过extension来给现有的类进行扩展(事实上Swift里面也有)比如我们现在对String类型扩展:
int.parse(this)
,只是调用者变成了String
本身1 | // 步骤一:扩展代码 |
显然,数字(比如200、200.0)有对应的包装类int、double,我们可以对其进行扩展:
对int类型扩展
1 | import '../utils/screen_adapter.dart'; |
对double类型扩展
1 | import '../utils/screen_adapter.dart'; |
使用
1 | import './extension/adapter.dart'; |
如果美工提供的设计稿为iPhone6尺寸的设计稿(最好不过),那么直接使用对着稿子量多少代码填多少rpx单位即可。
当使用extension时出现报错时,例如
1 | Undefined class 'extension'. |
解决办法:
在项目根目录下创建analysis_options.yaml
,并写入以下内容:
1 | include: |
以及设置pubspec.yaml
的environment
1 | environment: |
重启VSCode即可。
今天分享的面试题是:
Android在版本迭代中,总会进行很多改动,那么你熟知各版本都改动了什么内容?又要怎么适配呢?
ART
虚拟机,提供选项可以开启。HttpURLConnection
的底层实现改为了OkHttp。ART
成为默认虚拟机,完全代替Dalvik虚拟机。Context.bindService()
方法需要显式 Intent,如果提供隐式 intent,将引发异常。如果你的应用使用到了危险权限,比如在运行时进行检查和请求权限。checkSelfPermission()
方法用于检查权限,requestPermissions()
方法用于请求权限。
Android 6.0 版移除了对 Apache HTTP
相关类库的支持。要继续使用 Apache HTTP API,您必须先在 build.gradle 文件中声明以下编译时依赖项:
1 | android {useLibrary 'org.apache.http.legacy'} |
有的小伙伴可能不熟悉这是啥,简单说下:
Apache HttpClient 是Apache开源组织提供的一个开源的项目,它是一个简单的HTTP客户端(并不是浏览器),可以发送HTTP请求,接受HTTP响应。
所以说白了,其实就是一个请求网络的项目框架。
FileUriExposedException
异常,如调用系统相机拍照录制视频,或裁切照片。这一点其实就是限制了在应用间共享文件,如果需要在应用间共享,需要授予要访问的URI临时访问权限,我们要做的就是注册FileProvider
:
1)声明FileProvider。
1 | <provider |
2)编写xml文件,确定可访问的目录
1 | <paths xmlns:android="http://schemas.android.com/apk/res/android"> |
3)使用FileProvider
1 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
在 Android 8.0
之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。对于针对 Android 8.0 的应用,系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
也就是说,以前你申请了READ_EXTERNAL_STORAGE
权限,应用会同时给你授予同权限组的WRITE_EXTERNAL_STORAGE
权限。如果Android8.0以上,只会给你授予你请求的READ_EXTERNAL_STORAGE
权限。如果需要WRITE_EXTERNAL_STORAGE
权限,还要单独申请,不过系统会立即授予,不会提示。
Android 8.0 对于通知修改了很多,比如通知渠道、通知标志、通知超时、背景颜色。其中比较重要的就是通知渠道,其允许您为要显示的每种通知类型创建用户可自定义的渠道。
这样的好处就是对于某个应用可以把权限分成很多类,用户来控制是否显示哪些类别的通知。而开发者要做的就是必须设置这个渠道id,否则通知可能会失效。
1 | private void createNotificationChannel() { |
Android8.0以上必须使用新的窗口类型(TYPE_APPLICATION_OVERLAY
)才能显示提醒悬浮窗:
1 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
Android 8.0去除了“允许未知来源”选项,所以如果我们的App有安装App的功能(检查更新之类的),那么会无法正常安装。
1 | // <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> |
只有全屏不透明的activity
才可以设置方向。这应该是个bug,在Android8.0中出现,8.1中被修复。
我们的处理办法就是要么去掉设置方向的代码,要么舍弃透明效果。
https
。解决办法就是添加网络安全配置:1 | <application android:networkSecurityConfig="@xml/network_security_config"> |
在6.0中取消了对Apache HTTP
客户端的支持,Android9.0中直接移除了该库,要使用的话需要添加配置:
1 | <uses-library android:name="org.apache.http.legacy" android:required="false"/> |
Android 9.0 要求创建一个前台服务需要请求 FOREGROUND_SERVICE 权限,否则系统会引发 SecurityException。
1 | // <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> |
在9.0 中,不能直接非 Activity 环境中(比如Service,Application)启动 Activity,否则会崩溃报错,解决办法就是加上FLAG_ACTIVITY_NEW_TASK
1 | Intent intent = new Intent(this, TestActivity.class); |
Android10中默认开启了分区存储,也就是沙盒模式。应用只能看到本应用专有的目录(通过 Context.getExternalFilesDir()
访问)以及特定类型的媒体。
如果需要关闭这个功能可以配置:
1 | android:requestLegacyExternalStorage="true" |
分区存储下,访问文件的方法:
1)应用专属目录
1 | //分区存储空间 |
2)访问公共媒体目录文件
1 | val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc") |
3)SAF(存储访问框架–Storage Access Framework)
1 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) |
从Android10开始普通应用不再允许请求权限android.permission.READ_PHONE_STATE。而且,无论你的App是否适配过Android Q(既targetSdkVersion是否大于等于29),均无法再获取到设备IMEI等设备信息。
如果Android10以下设备获取设备IMEI等信息,可以配置最大sdk版本:
1 | <uses-permission android:name="android.permission.READ_PHONE_STATE" |
没错,Android11强制执行分区存储,也就是沙盒模式。这次真的没有关闭功能了,离Android11出来也有一段时间了,还是抓紧适配把。
改动了两个API:getLine1Number()和 getMsisdn() ,需要加上READ_PHONE_NUMBERS权限
你一定很奇怪,为什么Android11
的适配就这么草草收尾了?这可是我们最需要的啊?
哈哈,因为改动还是挺多的,所以给你推荐文章—Android11最全适配指南
,应该有很多朋友都看过了:https://juejin.cn/post/6860370635664261128,或者点击文末的“阅读原文”即可。
https://juejin.cn/post/6898176468661059597 https://blog.csdn.net/qq_17766199/article/details/80965631 https://weilu.blog.csdn.net/article/details/98336225
NSThread 基于OC的API,使用其简单,面向对象操作。但线程周期由程序员管理。
优点:轻量级
缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销
苹果推荐是用GCD 和 NSOperation
注意:
[NSThread currentThread]跟踪任务所在线程,适用于NSThread、NSOperation、GCD
使用NSThread的线程,不会自动添加autoreleasepool
线程中的自动释放池:
@autoreleasepool{}自动释放池。主线程中是有自动释放池,使用NSThread 和 NSObject 不会有。如果在后台线程中创建了autoreleasepool的对象,需要使用自动释放池,否则会出现内存泄漏。当自动释放池销毁时,对池中的所有对象发送release消息,清空自动释放池。当所有的autorelease对象,在出了作用域后,会自动添加到最近一次创建的自动释放池中。
1 | @property (nullable, copy) NSString *name //线程名字 |
1 | + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument; //开辟一个新的线程 |
1 | - (instancetype)init |
1 | - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument //此方法创建的线程需要手动启动 |
1 | //通过name属性设置线程名字 |
1 | - (void)start |
1 | + (void)sleepUntilDate:(NSDate *)date |
1 | - (void)cancel //当前正在执行的线程不会立刻停止 |
1 | + (void)exit |
1 | + (NSThread *)mainThread //获取主线程 |
1 | + (NSThread *)currentThread //获取当前线程 |
1 | * 通过executing属性判断线程是否正在执行 |
1 | - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array |
1 | - (void)performSelector:(SEL)aSelector onThread:(NSThread )thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray )array |
也可以通过NSPort对象实现通信
1.@synchronized(锁对象) { // 需要锁定的代码 }
2.只用一把锁,多锁是无效的
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源
运行flutter命令的时候显示出如下警告时
1 | Waiting for another flutter command to release the startup lock |
当项目异常关闭,或者android studio
用任务管理器强制关闭,下次启动就会出现上面的一行话。
此时需要打开${flutter的安装目录}/bin/cache/lockfile
,删除就行了。
或者直接用下面的命令:rm -rf ${flutter的安装目录}/bin/cache/lockfile
有时候,我们想要写一个能够跑在浏览器上和node上的包,但是由于二者在执行环境上有微弱的区别,比如浏览器上请求数据是用XMLHttpRequest对象,但是node上用的确是http或者https,诸如此类的差异还有很多。这就导致了我们要为浏览器端和node端准备不同的源文件,那我们要怎么区分不同的环境呢?
在此,以实现base64编码为例,在同一个入口文件,可以根据打包器提供的process.browser字段(在浏览器环境下为true,在node环境下为false)
1 | if (process.browser) { |
但是这种方式有一个很大的问题,打包器会在执行上诉代码中会为包引入polyfill,在这个例子中就是在浏览器中的Buffer实现buffer。这样打包出来的体积就会很大。
npm doc上的解释如下
If your module is meant to be used client-side the browser field should be used instead of the main field. This is helpful to hint users that it might rely on primitives that aren’t available in Node.js modules. (e.g. window)
总而言之就是在浏览器环境下用来替换main字段,包的作者可以通过browser字段提示包中要替换掉哪些模块或者要替换掉哪些源文件的实现。
browser的用法有以下几种
替换main成为浏览器环境的入口文件
1 | "browser": "./lib/browser/main.js" |
这种形式比较适合替换部分文件,不需要创建新的入口。key是要替换的module或者文件名,右侧是替换的
1 | "browser": {"module-a": "./browser/module-a.js","./server/module-b.js": "./browser/module-b.js"} |
打包器在打包到浏览器环境时,会将来自module-a
的替换为./browser/module-a.js
。将文件’./server/module-b.js’的引入替换为./browser/module-b.js
。
还可以使用布尔值防止将module加载到包中
1 | "browser": {"module-a":false,"./server/only.js":"./shims/server-only.js"} |
这种写法module-a
在浏览器环境中将不会被打包。
上面的所有写法的路径都是基于package.json
文件地址。
需要注意的是如果你的包能在浏览器和node上无差异化地实现,就不需要browser字段了。
[译] 怎样写一个能同时用于 Node 和浏览器的 JavaScript 包?
package.json 中 你还不清楚的 browser,module,main 字段优先级
package-browser-field-spec, 在 package.json 中,’browser’字段的规范文档
]]>发布NPM包时遇到的一些问题记录
1 | npm ERR! publish Failed PUT 403 |
这是注册的npm账号邮箱未进行验证,先去验证。一开始出现这个原因我是邮箱填错一直没收到邮件。
在发布npm
包的时候可能会出现报错信息:
1 | npm ERR! 403 403 Forbidden - PUT https://registry.npm.taobao.org/@hackycy%2fegg-typeorm - [no_perms] Private mode enable, only admin can publish this module |
出现这个问题是因为当前设置的是cnpm
登录到的是cnpm
,所以需要切换回来。
之前登录的时候就提出登录的是taobao
只不过那个时候没注意。
可以输入一下命令查看当前的登录源:
1 | $ npm config get registry |
可以看到返回的地址是淘宝源,需要切回到npmjs源,输入以下命令:
1 | $ npm config set registry=http://registry.npmjs.org |
设置完之后在查看当前登录的源地址:
1 | $ npm config get registry |
然后重新npm login
再发布即可。
1 | npm ERR! publish Failed PUT 403 |
你的包和别人的包重名了,npm 里的包不允许重名,所以去 npm 搜一下,改个没人用的名字就可以了。或者用@your-name/your-package
来命名。
无法发布私有包:
1 | npm ERR! publish Failed PUT 402 |
大多数是因为当你的包名为@your-name/your-package
时才会出现,原因是当包名以@your-name
开头时,npm publish
会默认发布为私有包,但是 npm 的私有包需要付费,所以需要添加如下参数进行发布:
1 | npm publish --access public |
https://blog.csdn.net/weixin_38080573/article/details/88080556
]]>在使用el-tree组件在获取完数据进行页面回显数据时,因为后端返回的数据中包含父节点的关系,但是子节点并没有全部选中,就把不该选中的子节点也全部勾上了。
isLeaf(判断节点是否为叶子节点)
getNode(获取tree中对应的节点)
setChecked (设置tree中对应的节点为选中状态)
1 | let res = [1,11,23,25,28,37]; |
注意,手动更新节点后无需在用回显数据赋值给data中绑定的值,否则会无效
在获取选择的节点数据时,子节点未全部选中时,getCheckedKeys
中没有包含父节点id。
1 | getCheckedKeys() { |
最近在翻查egg-cool-router源码查看如何使用装饰器来注册egg路由,看到了一些很牛逼的操作,想知道为什么就查了一下关于源码讲解的文章,解惑了所以摘抄了下来。
在学习egg-core是什么之前,我们先了解一下关于Egg框架中应用,框架,插件这三个概念及其之间的关系:
1 | # 加载单元的目录结构如下图,其中插件和框架没有controller和router.js |
egg.js的大部分核心代码实现都在egg-core库中,egg-core主要export四个对象:
所以egg-core做的主要事情就是根据loadUnit的目录结构规范,将目录结构中的config,controller,service,middleware,plugin,router等文件load到app或者context上,开发人员只要按照这套约定规范,就可以很方便进行开发,以下是EggCore的exports对象源码:
1 | //egg-core源码 -> index文件导出的数据结构 |
EggCore类是算是上文提到的框架范畴,它从Koa类继承而来,并做了一些初始化工作,其中有三个主要属性是:
1 | //egg-core源码 -> EggCore类的部分实现 |
如果说eggCore是egg框架的精华所在,那么eggLoader可以说是eggCore的精华所在,下面我们主要从EggLoader的实现细节开始学习eggCore这个库:
EggLoader首先对app中的一些基本信息(pkg/eggPaths/serverEnv/appInfo/serverScope/baseDir等)进行整理,并且定义一些基础共用函数(getEggPaths/getTypeFiles/getLoadUnits/loadFile),所有的这些基础准备都是为了后面介绍的几个load函数作准备,我们下面看一下其基础部分的实现:
1 | //egg-core源码 -> EggLoader中基本属性和基本函数的实现 |
上文中只是介绍了EggLoader中的一些基本属性和函数,那么如何将LoadUnits中的不同类型的文件分别加载进来呢,egg-core中每一种类型(service/controller等)的文件加载都在一个独立的文件里实现。比如我们加载controller文件可以通过’./mixin/controller’目录下的loadController完成,加载service文件可以通过’./mixin/service’下的loadService函数完成,然后将这些方法挂载EggLoader的原型上,这样就可以直接在EggLoader的实例上使用
1 | //egg-core源码 -> 混入不同目录文件的加载方法到EggLoader的原型上 |
我们按照上述loaders中定义的元素顺序,对各个load函数的源码实现进行一一分析:
插件是一个迷你的应用,没有包含router.js和controller文件夹,我们上文也提到,应用和框架里都可以包含插件,而且还可以通过环境变量和初始化参数传入,关于插件初始化的几个参数:
1 | //egg-core源码 -> loadPlugin函数部分源码 |
配置信息的管理对于一个应用来说非常重要,我们需要对不同的部署环境的配置进行管理,Egg就是针对环境加载不同的配置文件,然后将配置挂载在app上,
加载config的逻辑相对简单,就是按照顺序加载所有loadUnit目录下的config文件内容,进行合并,最后将config信息挂载在this对象上,整个加载函数请看下面源码:
1 | //egg-core源码 -> loadConfig函数分析 |
这里的loadExtend是一个笼统的概念,其实是针对koa中的app.response,app.respond,app.context以及app本身进行扩展,同样是根据所有loadUnits下的配置顺序进行加载
下面看一下loadExtend这个函数的实现,一个通用的加载函数
1 | //egg-core -> loadExtend函数实现 |
loadService函数的实现是所有load函数中最复杂的一个,我们不着急看源码,先看一下service在egg框架中如何使用
1 | //egg-core源码 -> 如何在egg框架中使用service |
我们上面列举了service下的js文件的四种写法,都是从每次请求的上下文this.ctx获取到service对象,然后就可以使用到每个service文件导出的对象了,这里主要有两个地方需要注意:
为什么我们可以从每个请求的this.ctx上获取到service对象呢:
看过koa源码的同学知道,this.ctx其实是从app.context继承而来,所以我们只要把service绑定到app.context上,那么当前请求的上下文ctx自然可以拿到service对象,eggLoader也是这样做的
针对上述四种使用场景,具体导出实例是怎么处理的呢?
在实现loadService函数时,有一个基础类就是FileLoader,它同时也是loadMiddleware,loadController实现的基础,这个类提供一个load函数根据目录结构和文件内容进行解析,返回一个target对象,我们可以根据文件名以及子文件名以及函数名称获取到service里导出的内容,target结构类似这样:
1 | { |
下面我们先看一下fileLoader这个类的实现
1 | //egg-core源码 -> FileLoader实现 |
上文中说道loadService函数其实最终把service对象挂载在了app.context上,所以为此提供了ContextLoader这个类,继承了FileLoader类,用于将FileLoader解析出来的target挂载在app.context上,下面是其实现:
1 | //egg-core -> ContextLoader类的源码实现 |
有了ContextLoader类,那实现loadService函数就非常容易了,如下:
1 | //egg-core -> loadService函数实现源码 |
中间件是koa框架中很重要的一个环节,通过app.use引入中间件,使用洋葱圈模型,所以中间件加载的顺序很重要。
1 | //egg-core -> loadMiddleware函数实现源码 |
controller中生成的函数最终还是在router.js中当作一个中间件使用,所以我们需要将controller中内容转换为中间件形式async function(ctx, next),其中initializer这个函数就是用来针对不同的情况将controller中的内容转换为中间件的,下面是loadController的实现逻辑:
1 | //egg-core源码 -> loadController函数实现源码 |
loadRouter函数特别简单,只是require加载一下app/router目录下的文件而已,而所有的事情都交给了EggCore类上的router属性去实现
而router又是Router类的实例,Router类是基于koa-router实现的
1 | //egg-core源码 -> loadRouter函数源码实现 |
Router类继承了KoaRouter类,并对其的method相关函数做了扩展,解析controller的写法,同时提供了resources方法,为了兼容restAPI的方式
关于restAPI的使用方式和实现源码我们这里就不介绍了,可以看官方文档,有具体的格式要求,下面看一下Router类的部分实现逻辑:
1 | //egg-core源码 -> Router类实现源码 |
以上便是对EggCore的大部分源码的实现的学习总结,其中关于源码中一些debug代码以及timing运行时间记录的代码都删掉了,关于app的生命周期管理的那部分代码和loadUnits加载逻辑关系不大,所以没有讲到。EggCore的核心在于EggLoader,也就是plugin,config, extend, service, middleware, controller, router的加载函数,而这几个内容加载必须按照顺序进行加载,存在依赖关系,比如:
装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。
注意 装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json
里启用experimentalDecorators
编译器选项:
命令行:
1 | tsc --target ES5 --experimentalDecorators |
tsconfig.json:
1 | { |
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上,可以修改类的行为。 装饰器使用 @expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
例:
1 | '/hello') ( |
装饰器本身其实就是一个函数,理论上忽略参数的话,任何函数都可以当做装饰器使用。例:
demo.ts
1 | function Path(target:any) { |
使用tsc
编译后,执行命令node demo.js
,输出结果如下:
1 | I am decorator. |
修饰器对类的行为的改变,是代码编译时发生的(不是TypeScript编译,而是js在执行机中编译阶段),而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
在Node.js环境中模块一加载时就会执行
但是实际场景中,有时希望向装饰器传入一些参数, 如下:
1 | "/hello", "world") ( |
此时上面装饰器方法就不满足了(VSCode编译报错),这是我们可以借助JavaScript中函数柯里化特性
1 | function Path(p1: string, p2: string) { |
在TypeScript中装饰器可以修饰四种语句:类,属性,访问器,方法以及方法参数。
应用于类构造函数,其参数是类的构造函数。
注意class
并不是像Java那种强类型语言中的类,而是JavaScript构造函数的语法糖。
1 | function Path(path: string) { |
它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰会在运行时传入下列3个参数:
1 | function GET(url: string) { |
注意:在vscode编辑时有时会报作为表达式调用时,无法解析方法修饰器的签名。
错误,此时需要在tsconfig.json中增加target配置项:
1 | { |
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1 | function PathParam(paramName: string) { |
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
1 | function DefaultValue(value: string) { |
1 | function ClassDecorator() { |
输出结果:
1 | I am property decorator |
从上述例子得出如下结论:
1、有多个参数装饰器时:从最后一个参数依次向前执行
2、方法和方法参数中参数装饰器先执行。
3、类装饰器总是最后执行。
4、方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。上述例子中属性和方法调换位置,输出如下结果:
1 | I am parameter2 decorator |
项目之前使用了第三方库的时候,对于FileProvider
的适配还不是很了解,因为使用时第三方库已经进行了适配。但是自己去覆写别人的第三方库的时候了解到了FileProvider
的适配。
对于Android 7.0,提供了非常多的变化,详细的可以阅读官方文档Android 7.0 行为变更,但是该文章主要叙述关于FileProvider
的适配。
在官方7.0的以上的系统中,尝试传递
file://URI
可能会触发FileUriExposedException
。
先来一个常用的例子,大家应该对于手机拍照一定都不陌生,在希望得到一张高清拍照图的时候,我们通过Intent会传递一个File的Uri给相机应用。
大致代码如下:
1 | private static final int REQUEST_CODE_TAKE_PHOTO = 0x110; |
未处理6.0权限,有需要的自行处理下,nexus系列如果未处理,需要手动在设置页开启存储权限。
此时如果我们使用Android 7.0或者以上的原生系统,再次运行一下,你会发现应用直接停止运行,抛出了android.os.FileUriExposedException
:
1 | Caused by: android.os.FileUriExposedException: |
原因在官网已经给了解释:
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
同样的,官网也给出了解决方案:
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。
https://developer.android.com/about/versions/nougat/android-7.0-changes.html#accessibility
FileProvider
属于Android 7.0新增的一个类,该类位于v4或者androidx包下,详情可见android.support.v4.content.FileProvider
或者androidx.core.content.FileProvider
,使用方法类似与ContentProvider
,简单概括为三个步骤,这里先以调用系统相机拍照并保存sdcard公共目录为例,演示使用过程:
res/xml
下新建file_paths.xml
文件,文件声明权限请求的路径,代码如下:1 |
|
要使用
content://uri
替代file://uri
,需要一个虚拟的路径对文件路径进行映射,所以需要编写个xml文件,通过path以及xml节点确定可访问的目录,通过name属性来映射真实的文件路径。
AndroidManifest.xml
添加组件provider
相关信息,类似组件activity
,指定resource
属性引用上一步创建的xml文件(后面会详细介绍各个属性的用法),代码如下:1 | <!-- 定义FileProvider --> |
getUriForFile()
和grantUriPermission()
,代码如下(后面会详细介绍方法对应参数的使用):1 | public void takePhotoNoCompress(View view) { |
通过FileProvider把
file
转化为content://uri
了
核心代码就这一行了~
1 | FileProvider.getUriForFile(this, "com.siyee.android7.fileprovider", file); |
第二个参数就是我们配置的authorities
,这个很正常了,总得映射到确定的ContentProvider吧~所以需要这个参数。
然后再看一眼我们生成的uri:
1 | content://com.siyee.android7.fileprovider/external/20200819-041411.png |
可以看到格式为:content://authorities/定义的name属性/文件的相对路径
,即name
隐藏了可存储的文件夹路径。
如果使用以上代码跑在7.0以上系统的手机没有问题,但是拿回到低版本的手机又会出现崩溃:
1 | Caused by: java.lang.SecurityException: Permission Denial: opening provider androidx.core.content.FileProvider from ProcessRecord{52b029b8 1670:com.android.camera/u0a36} (pid=1670, uid=10036) that is not exported from uid 10052 |
因为低版本的系统,仅仅是把这个当成一个普通的Provider在使用,而我们没有授权,contentprovider
的export设置的也是false;导致Permission Denial
。
而解决的办法就是授权了。通过grantUriPermission(String toPackage, Uri uri,int modeFlags)
和revokeUriPermission(Uri uri, int modeFlags)
方法。
可以看到grantUriPermission
需要传递一个包名,就是你给哪个应用授权,但是很多时候,比如分享,我们并不知道最终用户会选择哪个app,所以我们可以这样:
1 | List<ResolveInfo> resInfoList = context.getPackageManager() |
根据Intent查询出的所以符合的应用,都给他们授权~~
恩,你可以在不需要的时候通过revokeUriPermission
移除权限~
那么增加了授权后的代码是这样的:
1 | public void takePhotoNoCompress(View view) { |
但是这样的做法相对麻烦,我们可以对系统版本进行判断高版本用FileProvider.getUriForFile
,低版本继续使用Uri.fromFile
即可。
1 | Uri fileUri = null; |
直接使用FileProvider
本身或者它的子类,需要在AndroidManifest.xml
文件中声明组件的相关属性,包括:
android:name
,对应属性值:android.support.v4.content.FileProvider
或者子类完整路径android:authorities
,对应属性值是一个常量,通常定义的方式packagename.fileprovider
,例如:cn.teachcourse.fileprovider
android:exported
,对应属性值是一个boolean变量,设置为false
android:grantUriPermissions
,对应属性值也是一个boolean变量,设置为true
,允许获得文件临时的访问权限1 | <manifest> |
想要关联res/xml
文件夹下创建的file_paths.xml
文件,需要在<provider>
标签内,添加<meta-data>
子标签,设置<meta-data>
标签的属性值,包括:
android:name
,对应属性值是一个固定的系统常量android.support.FILE_PROVIDER_PATHS
android:resource
,对应属性值指向我们的xml文件@xml/file_paths
在xml文件中指定文件存储的区块和区块的相对路径,在<paths>
根标签中添加<files-path>
子标签(稍后详细列出所有子标签),设置子标签的属性值,包括:
name
,是一个虚设的文件名(可以自由命名),对外可见路径的一部分,隐藏真实文件目录path
,是一个相对目录,相对于当前的子标签<files-path>
根目录<files-path>
,表示内部内存卡根目录,对应根目录等价于Context.getFilesDir()
,查看完整路径:/data/user/0/com.siyee.demos/files
1 | <paths xmlns:android="http://schemas.android.com/apk/res/android"> |
<paths>
根标签下可以添加的子标签也是有限的,参考官网的开发文档,除了上述的提到的<files-path>
这个子标签外,还包括下面几个:
<cache-path>
,表示应用默认缓存根目录,对应根目录等价于getCacheDir()
,查看完整路径:/data/user/0/com.siyee.demos/cache
<external-path>
,表示外部内存卡根目录,对应根目录等价于Environment.getExternalStorageDirectory()
,/storage/emulated/0
<external-files-path>
,表示外部内存卡根目录下的APP公共目录,对应根目录等价于Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
,/storage/emulated/0/Android/data/com.siyee.demos/files/Download
<external-cache-path>
,表示外部内存卡根目录下的APP缓存目录,对应根目录等价于Context.getExternalCacheDir()
,查看完整路径:/storage/emulated/0/Android/data/com.siyee.demos/cache
最终,在file_provider.xml
文件中,添加上述5种类型的临时访问权限的文件目录,代码如下:
1 |
|
Content URI方便与另一个APP应用程序共享同一个文件,共享的方式通过ContentResolver.openFileDescriptor
获得一个ParcelFileDescriptor
对象,读取文件内容。那么,如何生成一条完整的Content URI呢?TeachCourse总结后,概括为三个步骤,第一步:明确上述5种类型中的哪一种,第二步:明确指定文件的完整路径(包括目录、文件名),第三步:调用getUriForFile()
方法生成URI
1 | File imagePath = new File(Environment.getExternalStorageDirectory(), "download"); |
上一步获得的Content URI,并没有获得指定文件的读写权限,想要获得文件的读写权限需要调用Context.grantUriPermission(package, Uri, mode_flags)
方法,该方法向指定包名的应用程序申请获得读取或者写入文件的权限,参数说明如下:
package
,指定应用程序的包名,Android Studio真正的包名指build.gradle
声明的applicationId属性值;getPackageName()
指AndroidManifest.xml
文件声明的package属性值,如果两者不一致,就不能提供getPackageName()
获取包名,否则报错!Uri
,指定请求授予临时权限的URI,例如:contentUri
mode_flags
,指定授予临时权限的类型,选择其中一个常量或两个:Intent.FLAG_GRANT_READ_URI_PERMISSION
,Intent.FLAG_GRANT_WRITE_URI_PERMISSION
授予文件的临时读取或写入权限,如果不再需要了,TeachCourse该如何撤销授予呢?撤销权限有两种方式:第一种:通过调用revokeUriPermission()
撤销,第二种:重启系统后自动撤销
正常我们在编写安装apk的时候,是这样的:
1 | public void installApk(View view) { |
拿个7.0的原生手机跑一下,android.os.FileUriExposedException
又来了~~
1 | android.os.FileUriExposedException: file:///storage/emulated/0/testandroid7-debug.apk exposed beyond app through Intent.getData() |
好在有经验了,简单修改下uri的获取方式。
1 | if (Build.VERSION.SDK_INT >= 24) { |
再跑一次,没想到还是抛出了异常(警告,没有Crash):
1 | java.lang.SecurityException: Permission Denial: |
可以看到是权限问题,对于权限我们刚说了一种方式为grantUriPermission
,这种方式当然是没问题的啦~
加上后运行即可。
其实对于权限,还提供了一种方式,即:
1 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); |
我们可以在安装包之前加上上述代码,再次运行正常啦~
现在我有两个非常疑惑的问题:
Permission Denial
的问题?恩,之所以不需要权限,主要是因为Intent的action为ACTION_IMAGE_CAPTURE
,当我们startActivity
后,会辗转调用Instrumentation的execStartActivity
方法,在该方法内部,会调用intent.migrateExtraStreamToClipData();
方法。
该方法中包含:
1 | if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action) |
可以看到将我们的EXTRA_OUTPUT
,转为了setClipData
,并直接给我们添加了WRITE
和READ
权限。
注:该部分逻辑应该是21之后添加的。
因为addFlags主要用于setData
,setDataAndType
以及setClipData
(注意:4.4时,并没有将ACTION_IMAGE_CAPTURE
转为setClipData
实现)这种方式。
所以addFlags
方式对于ACTION_IMAGE_CAPTURE
在5.0以下是无效的,所以需要使用grantUriPermission
,如果是正常的通过setData分享的uri,使用addFlags
是没有问题的(可以写个简单的例子测试下,两个app交互,通过content://
)。
使用content://
替代file://
,主要需要FileProvider
的支持,而因为FileProvider
是ContentProvider
的子类,所以需要在AndroidManifest.xml
中注册;而又因为需要对真实的filepath
进行映射,所以需要编写一个xml
文档,用于描述可使用的文件夹目录,以及通过name
去映射该文件夹目录。
对于权限,有两种方式:
Intent.addFlags
,该方式主要用于针对intent.setData
,setDataAndType
以及setClipData
相关方式传递uri
的。grantUriPermission
来进行授权https://blog.csdn.net/lmj623565791/article/details/72859156
]]>最近某灯挂的厉害,导致访问Github等网站实在是太慢了。同事给了一些SSR的搭建方法。以此记录了下来。
选服务器
我是使用vultr,新建主机这里就不多详细叙述了。选择CentOS 7的即可。
新建主机后拿到IP地址查看下是否能够Ping通,Ping不同也无法使用SSH连接的。
SSH连接后敲命令搭建好SSR的服务器环境:
CentOS:
1 | $ yum install python-setuptools && easy_install pip |
Debian / Ubuntu:
1 | apt-get install python-pip |
Windows:
简单用法
1 | $ ssserver -p 443 -k password -m aes-256-cfb |
后台运行
1 | $ sudo ssserver -p 443 -k password -m aes-256-cfb --user nobody -d start |
-p
指定端口,-k
指定密码,-m
指定加密方式,客户端连接时都需要对应上
停止
1 | sudo ssserver -d stop |
检查日志
1 | $ sudo less /var/log/shadowsocks.log |
通过检查所有选项-h。您也可以使用配置文件。
docker
使用shadowsocks-libev方式部署:
安装yum-utils
1 | yum install -y yum-utils |
添加docker yum
包
1 | yum-config-manager \ |
安装最新版本的 Docker Engine
和containerd
1 | yum install docker-ce docker-ce-cli containerd.io |
启动 Docker。
1 | systemctl start docker |
开机自启动
1 | systemctl enable docker |
通过运行hello-world
映像验证 Docker Engine 是否已正确安装。
1 | docker run hello-world |
启动ss-server
1 | docker pull shadowsocks/shadowsocks-libev |
环境指定:
PASSWORD指定密码,METHOD指定加密方式,默认为aes-256-gcm,SERVER_PORT内置服务端口,默认为8388,请将主机端口映射至该端口即可。
打开小飞机,点击服务器,增加配置,根据服务器配置的填写即可。