Hooks
是一个新的概念,它允许你组成状态和副作用。它们允许你在组件之间重用有状态逻辑。
如果你已经使用 Preact
工作了一段时间,你可能会熟悉 render props
和higher order components
等模式,这些模式试图解决这些挑战。这些解决方案往往会使代码更难遵循,更抽象。钩子API 使得整齐地提取状态和副作用的逻辑成为可能,同时也简化了独立于依赖该逻辑的组件的单元测试。
钩子可以用在任何组件中,并且避免了类组件 API 所依赖的这个关键字的许多陷阱。钩子不是从组件实例中访问属性,而是依赖于闭包。这使得它们具有值约束,并消除了在处理异步状态更新时可能出现的一些陈旧数据问题。
有两种方式导入钩子:从preact/hooks
或preact/compat
。
理解钩子的最简单的方法是将它们与同等的基于类的Components
进行比较。
我们将使用一个简单的计数器组件作为我们的例子,它渲染了一个数字和一个将其增加一的按钮。
1 | class Counter extends Component { |
现在,这里有一个用钩子构建的等价函数组件:
1 | function Counter() { |
在这一点上,它们似乎非常相似,然而我们可以进一步简化钩子版本。
让我们将计数器逻辑提取到一个自定义的钩子中,使其易于在各个组件中重用。
1 | function useCounter() { |
注意:CounterA
和 CounterB
都是完全独立的。它们都使用useCounter()
自定义钩子,但每个钩子都有自己的钩子相关状态的实例。
觉得这看起来有点奇怪?你并不孤单!
我们很多人花了一段时间才习惯了这种做法。
许多钩子都接受一个参数,这个参数可以用来限制钩子的更新时间。Preact
会检查依赖关系数组中的每个值,并检查它是否在上次调用钩子后发生了变化。当没有指定依赖性参数时,钩子总是被执行。
在上面的useCounter()
实现中,我们向useCallback()
传递了一个依赖关系数组。
1 | function useCounter() { |
在这里传递value
会导致每当 value
改变时,useCallback
都会返回一个新的函数引用。这对于避免 “陈旧的闭包”是必要的,因为在这种情况下,回调总是会引用第一个渲染的 value
变量,当它被创建时,导致增量总是设置一个值为1
。
这样每次
value
发生变化时都会创建一个新的increment
回调。出于性能方面的考虑,使用useState
来更新状态值通常比使用依赖关系保留当前值要好。
下面我们就来看看如何在功能组件中引入状态逻辑。
在引入钩子之前,任何需要状态的地方都需要类组件。
这个钩子接受一个参数,这将是初始状态。当调用这个钩子时,会返回一个由两个变量组成的数组,第一个是当前状态,第二个是我们的状态设置器。
我们的setter
的行为类似于经典状态的setter
,它接受一个以currentState
为参数的值或函数。当你调用setter
而状态不同时,它将从使用了该useState
的组件开始重新渲染。
1 | import { h } from 'preact'; |
当我们的初始状态开销很大时,最好传递一个函数而不是一个值。
useReducer
钩子与 redux
有很相似的地方。与 useState
相比,当你有复杂的状态逻辑,下一个状态取决于上一个状态时,它更容易使用。
1 | const initialState = 0; |
在 UI 编程中,经常会有一些状态或结果需要耗时的计算。Memoization
可以缓存该计算结果,使其在使用相同的输入时可以重复使用。
通过useMemo
钩子,我们可以记住其计算结果,只有当其中一个依赖关系发生变化时才重新计算。
1 | const memoized = useMemo( |
不要在
useMemo
里面运行任何effectful
代码。副作用属于useEffect
。
useCallback
钩子可以用来确保只要没有依赖关系发生变化,返回的函数就会保持引用平等。当子组件依赖引用平等来跳过更新时,这可以用来优化子组件的更新(例如 shouldComponentUpdate
)。
1 | const onClick = useCallback( |
有趣的是:
useCallback(fn,deps)
相当于useMemo(()=>fn,deps)
。
要在一个功能组件中获取一个 DOM 节点的引用,有一个 useRef
钩子。它的工作原理类似于 createRef
。
1 | function Foo() { |
注意不要把
useRef
和createRef
混淆。
要在功能组件中访问上下文,我们可以使用 useContext
钩子,而不需要任何高阶或包装组件。
第一个参数必须是由 createContext
调用创建的上下文对象。
1 | const Theme = createContext('light'); |
副作用是许多现代 Apps 的核心。无论你是想从 API 中获取一些数据,还是想在文档上触发一个效果,你会发现 useEffect
几乎可以满足你所有的需求。这也是 hooks API 的主要优势之一,它重新塑造了你的思维,让你以效果而不是组件的生命周期来思考。
顾名思义,useEffect
是触发各种副作用的主要方式。如果需要的话,你甚至可以从你的effect
中返回一个清理函数。
1 | useEffect(() => { |
我们先从 Title 组件开始,它应该反映出文档的标题,这样我们就可以在浏览器标签页的地址栏中看到它。
1 | function PageTitle(props) { |
useEffect
的第一个参数是一个触发效果的无参数回调。在我们的例子中,我们只想触发它,当标题真的发生了变化。当它保持不变时,更新它就没有意义了。这就是为什么我们要用第二个参数来指定我们的依赖关系数组。
但有时我们有一个更复杂的用例。想象一下一个组件,当它挂载时需要订阅一些数据,而当它卸载时需要取消订阅。这也可以通过 useEffect
来完成。要运行任何清理代码,我们只需要在回调中返回一个函数。
1 | // Component that will always display the current window width |
清理函数是可选的。如果你不需要运行任何清理代码,你不需要在传递给
useEffect
的回调中返回任何东西。
该签名与 useEffect
相同,但它将在组件被扩散且浏览器有机会绘制时立即启动。
每当一个子组件抛出一个错误时,你可以使用这个钩子来捕捉它并向用户显示一个自定义的错误 UI。
1 | // error = The error that was caught or `undefined` if nothing errored. |
出于监控的目的,任何错误的通知服务都是非常有用的。为此,我们可以利用一个可选的回调,并将其作为 useErrorBoundary
的第一个参数。
1 | const [error] = useErrorBoundary(error => callMyApi(error.message)); |
一个完整的使用例子可能是这样的:
1 | const App = props => { |
]]>如果你过去一直在使用基于类的组件 API,那么这个 hook 本质上是
componentDidCatch
生命周期方法的替代品。这个钩子是在 Preact 10.2.0 中引入的。
计算机的起源是数学中的二进制计数法
阿拉伯数字由从 0 到 9 这样 10 个计数符号组成,并采取进位制法,每10进一位,2871为例
其中 ^ 表示幂或次方运算。十进制的数位(千位、百位、十位等)全部都是 10^n 的形式。需要特别注意的是,任何非 0 数字的 0 次方均为 1。在这个新的表示式里,10 被称为十进制计数法的基数
.
我们将基数改为2,就可以理解二进制的展示了.例如110101.
二进制的数位就是 2^n 的形式
我们只要基于基数的改动,就可以使用任一进制来展示我们的数字.
1 | io.open() |
将数值转换为字符串的tostring()
函数,以及将字符串转换为数值的tonumber()
函数,都可选使用第二个参数指定应用于转换的进制(2到36之间).
组成计算机系统的逻辑电路通常只有两个状态,即开关的接通与断开。
二进制 110101 向左移一位,就是在末尾添加一位 0,因此 110101 就变成了 1101010
如果将 1101010 换算为十进制,就是 106,你有没有发现,106 正好是 53 的 2 倍 > 二进制左移一位,其实就是将数字翻倍。
所谓数字溢出,就是二进制数的位数超过了系统所指定的位数。目前主流的系统都支持至少 32 位的整型数字
二进制 110101 向右移一位,就是去除末尾的那一位,因此 110101 就变成了 11010
ps. 我们将 11010 换算为十进制,就是 26,正好是 53 除以 2 的整数商
二进制右移一位,就是将数字除以 2 并求整数商的操作
1 | console.log(5 << 2); //返回值20 |
如果数字是 -53 呢?那么第 32 位就不是 0
负数:
会改变符号位
二进制的“1”和“0”分别对应逻辑中的“真”和“假”,可以针对位进行逻辑操作
逻辑“或”的意思是,参与操作的位中只要有一个位是 1,那么最终结果就是 1,也就是“真”。如果我们将二进制 110101 和 100011 的每一位对齐,进行按位的“或”操作,就会得到 110111。
同理,我们也可以针对位进行逻辑“与”的操作。“与”的意思是,参与操作的位中必须全都是 1,那么最终结果才是 1(真),否则就为 0(假)。如果我们将二进制 110101 和 100011 的每一位对齐,进行按位的“与”操作,就会得到 100001。
逻辑“异或”和“或”有所不同,它具有排异性,也就是说如果参与操作的位相同,那么最终结果就为 0(假),否则为 1(真)。所以,如果要得到 1,参与操作的两个位必须不同,这就是此处“异”的含义。我们将二进制 110101 和 100011 的每一位对齐,进行按位的“异或”操作,可以得到结果是 10110。
1 | var a=1; |
本部分用于说明流式(flow)属性和弹性(flex)单位. 使用它们可以创建非常灵活地适用于各种各样的视图(view)
和内容尺寸(content sizes)
的布局。
流式(flow)
属性和弹性(flex)
单位旨在解决目前现下的 CSS 无法解决或实现的一些问题:
(containers)
和视口(viewport)
中的元素的垂直和水平对齐;(visual order)
与DOM中的元素顺序不同时,可以使用静态布局。弹性布局(Flexible layouts)
是指使用弹性(flex)
长度单位和流(flow)
属性。 弹性长度单位(flex units)
允许使用在定义元素的尺寸(size)
、外边距(margin)
、 内边距(padding)
,作为包含块(containing block)
的剩余可用空间的一部分。弹性单位的值是一个十进制数字,数字后加上”*”(星号)作为单位识别符。
流(flow)属性用来定义包含块(contained blocks)
在正常流(position:static)
中的布局方法。或者可以说,流(flow) 定义了容器的布局管理器。此模块包含以下标准的布局管理器定义:
(vertical)
(horizontal)
(horizontal-flow)
(vertical-flow)
(templated layout)
弹性(Flex)单元的值可以认为是有弹簧张力的。它根据自己本身的”弹性”来自动调整尺寸和位置。如定义值如下:
上面图片中块的布局的HTML代码如下:
1 | <div class="container"> |
p元素有以下样式:
1 | p |
在弹性单位中,元素的尺寸是对剩余空间内的可用包含框的计算。
弹性单位值是在所有非弹性单位值计算完成后计算的——是布局算法的最后一步。在这一步中,它的值可能来于包含容器的剩余未分配空间。在容器的垂直和水平方向上,所有弹性值在这个剩余未分配空间通过竞争来得出自己的计算值。
弹性单位只适用于元素的CSS属性中的内边距(padding)
、外边距(margin)
、宽度(width)
、高度(height)
。 它也可以用在静态(static正常流)
、绝对定位(absolute)
的元素中。浮动元素(float)
不支持弹性单位——若为浮动元素指定了弹性单位值时会被当做auto
值。
在计算元素的最终尺寸时,弹性单元的值被解释为一个权重(weight)
。如果剩余空间的弹性值总和小于1,相应的剩余部分将保持未分配。若果弹性值总和大于或等于1,所有的剩余空间将使用该弹性值作为比例分配权重来分配。
例如,下面这个示例:
1 | #container { width:300px; } |
#container元素中的#element元素将会应用下面的这个计算尺寸:
1 | 弹性值总和 = 1* + 2* + 1* = 4*; |
弹性单位值的计算遵循所有的通常约束。 例如, 最小宽度(min-width
)和最大宽度(max-width
)定义可以”弹性”的宽度的边界。出于弹性单位计算的原因,初始(默认)的最小宽度属性值被解释为具有最小的内在价值(intrinsic value)。
CSS 的最小宽度(min-width)
、最大宽度(max-width)
、宽度(width)和最小高度(min-height)
、最大高度(max-height)
、高度(height)
属性值可以接受以下的指定值: 最小内在价值(min-intrinsic)'和最大内在价值
(max-intrinsic)’。
(min-intrinsic)
—— 在一些容器中,该值指在相应方向上无需溢出(overflow)
渲染时的最小长度。例如:对于设置overflow:auto
的元素,它的最小长度为显示时没有滚动条的最小长度。(max-intrinsic)
—— 在一些容器中,该值指所有的子元素显示无需换行时的最小长度。或者说,当某元素的高度为max-intrinsic
时,在水平布局(类似*rtl*/*ltr*)
的系统,或者垂直布局的系统(类似*ttb*
)上,最大内在价值(max-intrinsic)
指当该元素的宽度达到最小时的该元素最小高度。若为段落
<p>first second</p>
设置width:min-intrinsic
时,该段落的宽度为该段落中最宽的那个单词的宽度。
若为这个段落设置width:max-intrinsic
时,则它的宽度为该段落中所有单词和空格长度的总和——该段落将呈现为单行文本。
流(flow)属性定义了容器如何布局它的子元素们。或者说,它建立了容器元素的布局管理器。
‘flow’
该属性定义了子元素在正常流中的布局方法:
文档中使用的术语:
流式容器(flow container)
流式容器(flow container)
是指包含'flow'
样式属性且值不为default
的块元素。这种元素使用给定的布局管理器来布局它的块子元素。如果流式容器(flow container)
包含display
不为block
、list-item
、code>table
的子元素,则这些子元素会包装在一个匿名块或表容器以便参与流式布局。
流元素(flowed element)
流元素(flowed element)
的直接父元素为流式容器(flow container)
。这些元素被父流式容器的布局管理器用来替换。
垂直流比较接近标准的自上而下的块元素的布局方式,例如div
, ul
等等。和标准布局方式唯一的区别在于硫元素使用弹性单位时。
flow:vertical
容器中的所有静态子元素将会被替换为从上到下、一个接一个的根据容器宽度形成一个单一的列。其所包含的定义了弹性单位的子元素的宽度值将会使用容器的宽度值来计算。同样,被包含元素的垂直尺寸会使用容器的高度来计算。如果容器的高度未定义,或者高度定义为height:auto
,则将没有剩余空间来分配给弹性值,这样的话,在这个方向上的弹性值可以被忽略掉。
如果容器定义了高度,且它的高度大于被包含元素的最小价值高度(min-intrinsic)
,则存在剩余空间。在这样的容器里,这个空间被分配给定义了垂直弹性值的子元素。
例如,下面的样式:
1 | #container { height:100%; border:1px dotted; } |
当定义下面的HTML标记时:
1 | <div id="container"> |
则会将#first元素放在#container
的顶部,而将#second
元素放在它的底部。
在CSS中,被包含元素的垂直编辑通常会叠到一起。 这里唯一需要注意的时当堆叠到垂直边距包含弹性值,而对应的另一个元素是固定值时。这种情况下,这个固定值会作为两个元素间弹性计算值的”最小约束(min-constraint)
“。这样,边距是弹性的,但是它不能小于这个固定值。
这是一个单行布局。 设置flow:horizontal
的容器的所有静态子元素会水平地一个接一个的排列成一行。布局是相对于容器的方向direction
属性进行。
在水平方向上,若子元素的宽度(width)
、左右外边距(margin)
、边框(border)
、内边距(padding)
给定了一个弹性值,它们将参与剩余空间的分配。flow:horizontal
容器的所有直接子元素将对容器内容区(content box)
中左右边界间的剩余空间进行计算,使这个空间根据它们的弹性值进行分配。
在垂直方向上:被包含元素的高度、上下外边距、边框、内边距的弹性值使用容器的高度值来计算。这使flow:horizontal
容器的子元素排序不仅是水平地,也可以是垂直的。
下面的样式中,所有的子元素拥有相同的高度:
1 | #container { flow:horizontal; border-spacing:4px; padding:4px; } |
渲染结果如下:
下面,所有的子元素都设置为最小内在价值高度。而将上外边距设置为1*
:
1 | #container { flow:horizontal; border-spacing:4px; padding:4px; } |
flow:horizontal
容器中包含块的水平外边距堆叠处理方式和flow:vertical
容器相同。而对于in-flow
子元素,它们不与flow:horizontal
容器的外边距堆叠。
flow:horizontal
容器的内在高度(Intrinsic height
)是在它内部的一行中最高的元素的外边距框高度。 flow:horizontal
容器的内在宽度是所有子元素在水平边距堆叠的情况下的最小内在宽度之和。
flow:horizontal-flow
布局是flow:horizontal
布局的一种变种。该布局允许容器的子元素在水平方向上没有足够空间时换行。
clear:left|right|both
属性可以明确的中断元素布局流,使其成为多行。
在满足以下基本条件之一时,允许换行:
clear:left|right|both
属性;在垂直方向上,设置弹性值的包含块的高度、上下外边距、边框、内边距的值是使用当前行的高度。该行的高度等于在不影响弹性值计算的情况下该行中最高元素的高度。
在水平方向上,一行中的元素的弹性值计算和flow:horizontal
布局相同。
例如, 下面的HTML标记语言:
1 | <div style="flow:horizontal-flow" > |
渲染结果如下:
flow:vertical-flow
布局类是一个多列布局,似于flow:horizontal-flow
。在垂直方向上,元素会从上到下的排列放置。如果容器没有足够的垂直空间,元素会换列,变成多列布局。
clear:left|right|both
属性允许中断列,使其明确地成为多列布局。
在满足一下条件之一时,会进行换列:
clear:left|right|both
属性在某个元素中明确使用;在水平方向上,容器内的子元素的宽度、左右外边距、内边距的弹性值使用当前列的宽度来计算。该行的宽度等于在不影响弹性值计算的情况下该行中最宽元素的宽度。
在垂直方向上,某列中元素的弹性值计算方法与flow:vertical
布局相同。
例如,下面的HTML标记:
1 | <ul style="flow:vertical-flow"> |
每个列表项设置了width:150px
,这会生成下面的布局,列表项会处理成3列:
请注意,该布局是http://www.w3.org/TR/css3-layout/. 的一个简化版本,该想法的所有版权属于该文档的作者。
flow: <模板表达式>
允许根据模板表达式来替换放置元素。
在这里,模板表达式是一个字符串标识序列。每个字符串标识是一个使用空格分隔的名称标识列表中的一项,其中每个标识指定一个网格中的单元格。多列允许有相同的名称。在这种情况下,该标识相当于定义了一个横跨多个单元网格的占位符。
例如, 下面的模板定义了从”a”到”f”的一个3x4的共6个占位网格的表格。某些占位网格跨越了多个单元格:
1 | flow: "a a a" |
容器中的每个子元素使用 float:"占位标识名称"
属性绑定到模板定义的特定的占位网格对应的位置:
1 | li:nth-child(1) { float:"a"; } |
注意,在使用该流式(flow)布局的容器内的直接子元素使用
float:left|right
将不起作用。或者说,被流式布局的元素只能使用flow:"template"
定义的单元格来布局。
模板容器中的所有没有绑定到网格的子元素将会作为单独的一行追加到最后。如果有多个子元素有相同的占位符名称,只有第一个(DOM中的顺序)将被绑定到占位网格上,剩余的元素会变成未绑定的。
每个占位符名称在模板中必须是唯一的、矩形的。否则,该模板将无效,流式布局将采用默认的flow:default
.
例如,下面的HTML标记:
1 | <ul> |
设置样式后渲染结果如下:
除了用字符定义名称外,还可以使用子元素在模板容器中的顺序数字,所以下面的这个模板:
1 | flow: grid(1 1, |
会导致容器的前三个子元素布局到两行,且第一个元素放置在第一行,而第二、第三个元素放置在第二行。
flow:row()
函数用来实现类似table的布局。row()
函数的参数为元素标签的列表,该列表定义的元素将会放置在表格中的单独一行。
考虑下面的HTML标记:
1 | <dl><dt>第一项</dt> |
并且设置它的样式如下:
1 | dl { flow: row(dt,dd); } |
则它们将会渲染成如下:
第一项 | 第一项的描述 |
---|---|
第二项 | 第二项的描述 |
如果flow:row(...)
布局的元素内存在不匹配row中的模板的元素,则该元素将会被放置在单独的一行并跨越所有列。所有考虑下面的HTML标记:
1 | <dl> |
依然使用上面的样式,则它们会渲染成:
组别1 | |
---|---|
第一项 | 第一项的描述 |
第二项 | 第二项的描述 |
组别2 | |
第三项 | 第三项的描述 |
flow:row
的声明可以在一列中接受一个元素列表。如:
1 | flow: row(label, input select textarea); |
定义了两列,第一列放置了<label>
元素,而所有其他的<input>,<select>
和<textarea>
元素被放置在第二列。
flow:stack
布局用于在容器中的任意位置放置元素。渲染的顺序取决于元素的DOM位置或z-index
属性定义的顺序。
在水平和垂直方向上, 被包含元素的width、height、margin、padding
的弹性值使用容器元素的宽度和高度来计算。在弹性计算中,没有子元素都被当做容器元素的唯一子元素来对待 - 子元素的位置不会影响其他子元素的位置。
flow:stack
容器元素的内在尺寸等同于容器中子元素外边距盒的最宽的和最高值。
考虑下面的HTML代码:
1 | <section tab="标签页一"> |
它的样式为:
1 | section { flow: stack; width: max-content; } |
通过修改section
元素的 tab
属性值,我们可以切换标签页的显隐。
原则上,
flow:stack
布局在某些方面类似于在position:relative
容器中包含position:absolute
子元素。不过flow:stack
的内在尺寸计算规则是其他CSS属性无法模拟的。
那些flow
属性设置了非默认值的元素的直接子元素,将会建立一个新的块格式上下文,这个上下文类似于表格中的单元格。
流元素(flowed element
)是指在流容器中的position
为静态(默认)的子元素们。这意味着那些包含position: absolute | fixed
的子元素会被当做position:static
来处理。
在流元素中position:relative
是被允许的。因此,这些元素可以使用left
、right
、 bottom
和top
属性来定义它们相对于静态位置的偏移。
弹性单位可以用在定义了position:absolute
或position:fixed
的元素的left
、top
、right
和bottom
属性中,这些元素的内边距(padding
), 外边距(margin
), 宽度(width
)和高度(height
)中的弹性单位值的计算将会参考包含它们的父块。
例如:下面的样式将会将#light-box-dialog
元素放置在视口(viewport)
的中央:
1 | #light-box-dialog |
#light-box-dialo
g元素的宽度为400像素,高度为自动(即最小内在高度height:min-intrinsic
),且该元素将会放置在视口中央。
流元素建立了一个块格式上下文。因此它们的vertical-align
属性定义了它们内部垂直方向上的对齐方式,而不是它们本身在垂直方向上的对齐方式。或者说vertical-align
类似于表格中的单元格。
border-spacing
属性定义了两个流元素在水平和垂直方向上外边距的最小值。如果一个流元素定义了自己的外边距,则外边距使用的值是border-spacing定义值和该外边距定义中比较大的那个值。 如果这个外边距值使用了弹性单位值,则该弹性值的计算会将border-spacing值作为最小约束值。在这种情况下,计算出的弹性值不能小于border-spacing
属性值。
不同的布局管理器的对流元素的外边距堆叠处理是不一样的。流容器外边距不会与它内部的硫元素堆叠。
行内块放置在对应的行框中。原则上,行框是可以”弹性”的。
所以, 像<img>、<input>、<span style="display:inline-block">
这些inline-block元素可以使用弹性单位来定义它们的尺寸、外边距、内边距。行框上下文的弹性单位计算依据于行框的水平、垂直尺寸,而它们的内容将不是可弹性的。
在水平方向上,行框在分配完所有非弹性内容后可能存在剩余的空间(例如单词框)。这些空间将会在所有指定了弹性值的元素的宽度、左右外边距(或内边距)间分配。
在垂直方向上, 行内块(inline-block)
元素的高度、上下外边距、内边距中的弹性值依据于行框的高度。例如,可以定义多个和行框同高度的子元素。
如果这样定义了,text-align:justify
属性的计算将会放置弹性值计算之后。
例如下面的HTML标记:
1 | <style> |
这回到这p
元素各种各样的宽度值:
]]>注意,上面的最后一个图像上,span已经达到了它的最小内在宽度(min-intrinsic),所以它们已经排除掉了相关的弹性值计算。
当你点击EXE文件系统一个应用程序的时候 - 系统会创建一个进程(process)
而在一个进程内可以包含多个线程(thread)。用来显示界面的线程,我们通常称为“界面线程”,
其他不是用来显示界面的线程,我们一般称为“工作线程”或者是“后台线程”。
界面线程会使用 win.loopMessage();
启动一个消息循环,win.loopMessage();
就象一个快递公司不知疲倦的收发消息,直到用户关闭最后一个窗口他才会退出。
当然你也可以使用 win.quitMessage()
退出消息循环。
下面是一个启动界面线程的例子:
1 | import win.ui; |
你可以看到一个窗体显示在屏幕上,如果你去掉代码中的最后一句 win.loopMessage();
那么窗体只会显示一下就消失了,你的程序也迅速退出了。
但如果你加上 win.loopMessage();
窗体就会一直显示在屏幕上(直到你点击关闭按钮)。
并且你可以做其他的操作,例如点击按钮。
我们尝试点击按钮,点击按钮后触发了 winform.button.oncommand()
函数,
一件让我们困惑的事发生了,窗体卡死了任何操作都没有反应,这是因为类似 sleep(5000)
这样的耗时操作阻塞了win.loopMessage()
启动的消息循环过程。
一种解决方法是把 sleep(5000)
改成 win.delay(5000)
,虽然他们同样都是延时函数,但是win.delay()
会同时继续处理窗口消息。但如果我们不只是延时还要做其他耗时的操作,那就需要启动一个新的线程。
一个线程会排队执行一系列的编程指令,但一个线程同时只能做一件事。
例如在界面上有耗时的操作在执行时 - 就不能同时处理其他的界面消息或者响应用户的操作。
这时候我们就要使用多线程来完成我们的任务。
我们假设有一个耗时操作是这样的:
1 | //下面这个函数执行耗时操作 |
一般我们直接调用这个函数会是这样写:1
doSomething( "也可以有参数什么的" )
如果希望写复杂一点调用这个函数,我们也可以这样写:1
invoke(doSomething ,,"也可以有参数什么的" )
如果我们希望创建一个新的线程来调用这个函数,那么就需要下面这样写:1
thread.invoke(doSomething ,"也可以有参数什么的" )
切记不要犯一个低级错误:
如果把创建线程的代码改为thread.invoke( doSomething("也可以有参数什么的") )
这是在创建线程前就调用函数了,实际执行的代码是thread.invoke( 123 )
这肯定会出错的。
线程有独立的运行上下文,独立的变量环境
多线程最让人困惑的是线程间的同步和交互。
线程就象多个在并列的轨道上疾驰的火车,你要在A火车上与B火车上的人交互,或者你想让B火车上的人干什么,你不能直接从火车上把手伸出去跟别的火车上的人拉拉扯扯发生种种亲密的互动。
thread.lock()
,但实际上在aardio中多线程同步很少需要用到同步锁,所以这里我也就不多讲。如果你有一些函数需要被多个线程用到,请他们写到库文件里,然后在任何线程中使用 import
语句导入即可使用。
可以在创建线程时,通过线程的启动参数把变量从一个线程传入另一个线程,例如:
1 | thread.invoke( 线程启动函数,"给你的","这也是给你的","如果还想要上车后打我电话" ) |
多线程共享的变量,必须通过 thread.get()
函数获取,并使用 thread.set()
函数修改其值,thread.table
对象对这两个函数做了进一步的封装(伪装成一个普通的表对象)
aardio提供了很多线程间相互调用函数的方法,通过这些调用方式的传参也可以交互变量,具体请查看aardio范例中的多线程范例。
在aardio中每个线程有独立的运行上下文、线程有独立的变量环境,有独立的堆栈,所以你不能把包含局部变量闭包的对象从一个线程传到另一个线程,常见的就是调用类创建的对象,因为this就是闭包变量。
另外你也不可以在一个线程中引用库文件,并且把引用的库直接传到另一个线程,因为库文件中通常会大量的使用局部变量闭包,而应该在每个线程中自行导入需要用到的库,一个例子:
1 | import console; |
新手可能不太容易理解,aardio中的这种模式给多线程开发带来了巨大的方便,在aardio的多线程代码中基本很少看到同步锁,也很少会因为同步出现各种BUG和麻烦,以前面并列飞奔的多个火车来比喻,在aardio中每辆火车都只要愉快的往前跑就行了,不存在谁停下来等谁同步的问题。
但不可否认,工作线程中如果能直接操作窗口上的控件那会带来巨大的方便(因为工作线程需要访问界面控件的需求还是非常多的),但这违反了aardio的规则,在aardio的旧版本中这是行不通的,在aardio新版本中,我们愉快的解决了这个问题。现在aardio中可以存在一些特权对象,让一些不能在线程中直接传递的对象可以跨线程传递,例如窗口对象,下面我们看一个例子:
1 | import win.ui; |
注意上面的线程启动函数直接写在了参数里(匿名函数),跟下面的写法作用是一样的:
1 | //下面这个函数执行耗时操作 |
在工作线程中直接操作界面控件固然令人愉快,
但如果代码量一大,界面与逻辑混杂在一起,会让代码不必要的变的千头万绪复杂臃肿。
如果把多线程比作多条轨道上并列飞奔的火车,那么火车交互的方法不仅仅只有停下来同步,或者把手伸出车窗来个最直接的亲密交互。一种更好的方式是拿起手机给隔壁火车上的人打个电话 - 发个消息,或者等待对方操作完了再把消息发回来。
这种响应式的编程方式在aardio里就是 thead.command
,下面我们看一个简单的例子:
1 | import win.ui; |
thread.command
可以把多线程间复杂的消息交互伪装成普通的函数调用,非常的方便。
这里新手仍然可能会困惑一点:我在工作线程中不是可以直接操作界面控件么?! 你这个thread.command
虽然好用,但是多写了不少代码呀。
这样去理解是不对的,你开个轮船去对象菜市场买菜固然是有点麻烦,但如果你开轮船去环游世界那你就能感受到它的方便在哪里了。thread.command
一个巨大的优势是让界面与逻辑完全解耦,实现界面与逻辑的完全分离,当你的程序写到后面,代码越来越多,就能感受到这种模式的好处了。
例如 aardio自带的自动更新模块的使用示例代码:
1 | import fsys.update.dlMgr; |
这个fsys.update.dlMgr
里面就用到了多线程,但是他完全不需要直接操作界面控件。
而你在界面上使用这个对象的时候,你甚至都完全不用理会他是不是多线程,不会阻塞和卡死界面,有了结果你会收到通知,你接个电话就行了压根不用管他做了什么或者正在做什么。
这个fsys.update.dlMgr
里面就是使用thread.command
实现了实现界面与逻辑分离,你可以把检测、下载、更新替换并调整为不同的界面效果,但是fsys.update.dlMgr
的代码可以始终复用。
一般我们可以使用 thread.invoke()
函数简单快捷的创建线程,
而 thread.create()
的作用和用法与 thread.invoke()
一样,唯一的区别是 thread.create()
会返回线程句柄。
线程句柄可以用来控制线程(暂停或继续运行等等),
如果不再使用线程句柄,应当使用 raw.closehandle()
函数关闭线程句柄(这个操作不会关停线程)
有了线程句柄,我们可以使用 thread.waitOne()
等待线程执行完毕,
而且 thread.waitOne()
还可以一边等待一边处理界面消息(让界面不会卡死)。
下面看一下aardio范例里的多线程入门示例:
1 | import console; |
您可以使用 thread.command 在线程间交互通信,请参考《多线程开发入门》
您还可以使用 thread.event 来实现线程间的同步,请参考《多线程中的交通信号灯:thread.event》
或者使用 thread.works
、thread.manage
这些线程管理器来批量的管理线程句柄,
请参考此目录中的其他范例。
aardio中提供了 thread.manage
,thread.works
等用于管理多个线程的对象,
例如标准库中用于实现多线程多任务下载文件的 thread.dlManager
就使用了thread.works
管理线程。
thread.works 用于创建多线程任务分派,多个线程执行相同的任务,但可以不停的分派新的任务,
例子:
1 | import console; |
而
thread.manage
可以用来创建多个线程执行多个不同的任务,可以添加任意个线程启动函数,
在线程执行完闭以后可以触发onEnd
事件,并且把线程函数的返回值取回来,
示例如下:
1 | import console; |
thread.manage
通常是用于界面线程里管理工作线程,上面为了简化代码仅仅用到了控制台。
我们有时候在界面中创建一个线程,仅仅是为了让界面不卡顿,我们希望用 thead.waitOne()
阻塞等待线程执行完闭(界面线程同时可以响应消息),然后我们又希望在后面关闭线程句柄,并获取到线程最后返回的值。
可能我们希望一切尽可能的简单,尽可能的少写代码,并且也不想用到thread.manage
(因为并不需要管理多个线程)。
这时候我们可以使用 win.invoke
,win.invoke
的参数和用法与 thread.invoke
完全一样,
区别是 win.invoke
会阻塞并等待线程执行完毕,并关闭线程句柄,同时获取到线程函数的返回值。
示例:
1 | import win.ui; |
代码运行测试一下,在线程执行完以前,你仍然可以流畅的拖动窗口,操作界面。
重视范例,才能开箱即用!
教程中用到的多线程直接调用窗口对象的功能 - 需要更新到新版 aardio才能支持。
更多关于多线程的功能请大家看aardio范例和文档。
一些用户可能不明白 aardio怎样才能真正的“开箱即用”,我接触到的一些用户拿起aardio就可以直接使用,写出非常好的程序而且速度很快,他们高兴的表示aardio简洁轻巧不用特别的学习直接就可以使用,而另外一些却始终在犹豫,在到处找教程、找文档,始终找不到方法,每前进一步都要求你准备一大堆的说明书才敢向前迈一步,实际上我发现他们换其他编程工具也是类似的结果( 可能有极少数学步车式的开发工具他们会适应 )。
我也跟那些上手比较快的用户聊过一些,发现他们都有一个共同的习惯就是非常重视范例,
因为 aardio的范例非常、非常的多,而且aardio范例跟其他语言都有一些不同,很多代码就是几句代码就是一个简单而完整的程序,我经常听到一些人跟我说,仅仅是复制一些范例整合到一起,做一些修改就可以做出软件。
所以请记住:教程写的再多,看的再多,始终是纸上谈兵。
搞培训的人很愉快因为能赚到钱,而参加培训的人也很愉快因为找到了心理安慰剂,但真正能让你学会编程的是多看范例,多跑代码,多动手写代码!
]]>先使用go语言编写一个exe文件( 当然你可以把后缀名改为 dll,下面的代码一样可以运行 )
go语言代码如下,注意 go里面{换行写是语法错误 :
1 | package main |
假设上面用go语言生成的exe文件名为gotest.exe,并且是放在当前工程目录下,然后我们用下面的 aardio 代码调用这个 gotest.exe 里的go函数:
1 | import win.ui; |
附:调用Go语言编译器例子
1 | import console; |
最新版golang扩展库已支持自动下载配置Go编译器。
Go最新版已经支持调用生成DLL文件(需要调用gcc),在aardio中可以直接调用Go生成的DLL文件(使用cdecl调用约定),下面是调用Go编译器生成DLL的演示。
下面看演示:
1 | import console; |
Go写DLL要注意一个特别的地方,Go导出函数前必须写一行注释声明导出函数,例如上面的 //export SayHello
Go语言里的字符串GoString是一个结构体,用aardio来表示是这样的:
1 | class goString{ |
如果是在API函数里传值,一个GoString展开为2个API参数来表示就可以了(一个字符串,后面跟一个字符串长度)
因为aardio传结构体都是传指针,如果用结构体,在Go里面要声明为指针,示例:
1 | import console; |
需要先安装MinGW( GCC )
可以下载安装 MinGW-W64: https://sourceforge.net/projects/mingw-w64 这个只能安装在64位系统。
也可以下载安装 TDM-GCC: http://tdm-gcc.tdragon.net/download 这个提供支持32位、64位安装包。
golang扩展库会自动搜索MinGW,MinGW-W64,TDM-GCC的安装位置,不需要手动配置。
当然也可以调用golang扩展库提供的addPath函数自己添加gcc.exe所在的目录。
Go生成的文件很大,加上-ldflags "-s -w"
参数会小一些,go.buildShared()
已经自动加上这些参数。
编译上面的代码生成的DLL只有1MB多一点,而且可以支持WinXP,不需要依赖外部运行库,还是非常不错的。
而且测试了一下,编译的DLL还能内存加载。
web.rest
下面的支持库最简单的用法就是作为一个 HTTP客户端使用,该客户端对象简化了get
,post
,put
,patch
,delete
等常用的 HTTP请求操作,并提供编码请求数据、解码返回数据的功能。标准库中用于调用 REST API 的库:
web.rest.client
请求参数使用urlencode编码,服务器返回文本数据。web.rest.xmlClient
请求参数使用urlencode编码,服务器返回xml格式数据。 web.rest.jsonLiteClient
请求参数使用urlencode编码,服务器返回JSON格式数据。web.rest.jsonClient
请求参数与服务器返回数据都使用JSON格式。除了与服务器交互的数据格式不同以外, 这几个库的接口用法完全一样,可以看看这几个库的源码实际上他们都是调用 web.rest.client 这一个库。
web.rest下面的支持库最简单的用法就是作为一个HTTP客户端使用,该客户端对象简化了get
,post
,put
,patch
,delete
等常用的HTTP请求操作,并提供编码请求数据、解码返回数据的功能,下面是一个最简单的示例:
1 | import console; |
从上面的示例可以看出,我们上传参数的是aardio
中的对象,返回的数据也被自动解码为aardio
对象,虽然 HTTP传输使用的是 JSON
数据,但使用时不需要去管 JSON
的编解码等一系列的操作。
web.rest
不仅仅可以用来做上面这些简单的 HTTP请求、以及编解码的操作,他还可以将基本符合 REST风格的 Web API转换为aardio
中的函数对象,这非常有意思,REST本身不是一个严格的规范、更缺乏WebService
那样的WSDL
接口描述服务,但是aardio
设计了一种简单可行的声明语法,可以非常方便的把混乱的 Web API转换为统一的 aardio函数。
首先我们看一下 REST API的 URL 一般会是这种格式 http://主机/资源目录名/资源目录名/资源名aardio
的web.rest
库模块中的客户端对象提供一个 api 函数用于声明一个API接口,api 函数的定义如下:
var restApi = restClient.api("接口URL描述","默认HTTP请求动词")
其中接口URL描述可以直接指定一个web api的网址,在该网址中还可以使用变量,变量放在花括号中,例如:http://主机/{变量名}/资源目录名/资源名 aardio
并不关心变量名的内容是什么,只关心它们出现的前后顺序,当调用restApi
的成员函数时会使用函数名替换接口 URL 中的变量生成新的请求URL。
下面是一个简单的示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import console;
import web.rest.jsonClient;
// 创建REST客户端
var restClient = web.rest.jsonClient();
//声明一个API接口,第一个参数指定URL描述
var restApi = restClient.api("http://httpbin.org/api/{program}/{lang}")
/*
下面调用接口函数,
在请求时下面代码中的接口名"language"替换接口URL描述中的变量{program}
接口名"aardio"则替换接口URL描述中的变量{lang}
最后生成的请求URL为 http://httpbin.org/api/language/aardio
*/
var result = restApi.language.aardio()
console.log("请求的URL", restClient.lastRequestUrl)
restClient.lastResponse(); //输出服务端最后返回的数据
console.pause();
接口 URL中连接的变量名还可以合并为{...}
例如 http://httpbin.org/api/{program}/{lang}
可以简写为 http://httpbin.org/api/{...}
当 {...}
出现在尾部时还可以直接省略,例如 http://httpbin.org/api/
注意head,get,post,put,patch,delete等默认的HTTP请求操作作为函数名时不会被添加到生成的URL中。
这些默认的HTTP方法名在 web.rest.client._defaultMethod
中指定,例如使用 restApi.language.get()
显示的指定 HTTP请求动词为GET
。如果不指定 HTTP请求动词,则使用调用 restClient.api("接口URL描述","默认HTTP请求动词")
函数时第二个参数指定的 HTTP请求动词,不指定该参数时默认为POST
。
HTTP规定了九种动词(Verbs)
用于指定请求方法:GET
,HEAD
,POST
,DEBUG
,PUT
,DELETE
,PATCH
,OPTIONS
,
而在REST API中
用到的有五种 GET
,POST
,PUT
,DELETE
,PATCH
,他们的用途如下:
GET
:用于获取数据POST
: 用于创建数据 PUT
: 用于替换数据、也可用于更新数据DELETE
: 用于删除数据PATCH
:用于更新数据如果一个REST API在请求时需要上传、下载文件,那么所有调用规则如前不变。
你仅仅需要做的是,在调用API以前指定接受、或发送文件的回调函数以获取上传、下载的进度。
上传文件示例:
1 | restClient.sendFile( "上传文件路径" |
下载文件示例:
1 | restClient.receiveFile( "上传文件路径" |
web.rest
也可以支持 multipart/form-data
编码上传文件,示例:
1 | import console; |
web.rest
客户端对象的错误处理与inet.http
相同:
请求成功返回服务器数据,失败返回空值,错误信息,错误代码等。
注意下面为了演示所有的细节,代码写的比较长,实际开发中不必要写的这么细
1 | import console; |
当然上面的代码一般在调试故障时才需要,一般没必要把错误处理写的这么细,上面的代码也可以简化如下:
1 | import web.rest.jsonLiteClient; |
本文参考了 REST API的设计风格、但有部分规则有所变通。在实践中 REST API 完全符合REST规则的比较少见,我在设计 web.rest.client 试图编写一个尽可能通用的支持库、但是发现REST API的实现真是五花八门,而且也缺乏一个统一的接口描述规则。
如果你需要为你的aardio客户端程序设计自己的Web API,那么参考下面的几条原则去实现服务端接口 - 这可以让标准库里 web.rest.client 方便的支持该API( 请参考:使用 web.rest 调用 REST API ).
URL应用于清晰的展现资源定位路径,目录应当使用清晰的资源名称,并可以使用统一的URL接口描述语法声明该API。
例如:http://主机/资源分类/资源目录/资源名/资源ID 原则上不应当把资源名放在URL参数里,
但是要使用这样的友好URL在现实中是用一定代价的,对于一般的Web服务器这可能需URL重写,有一定的性能负担。所以也可以将资源名放到URL参数里,例如 HTTP://主机/资源分类?资源目录=目录名&资源名=资源名&资源ID=资源ID 要注意这里的资源定位有关的参数应当直接放到URL的参数里也就是?号后面,要将资源定位的参数与HTTP提交的参数分开来( 如果你用过 web.rest.client
就知道为什么要这样做 ),并且要按资源定位关系决定参数出现的先后关系。
最后生成的URL要能使用以下的URL描述规则:
URL中的资源名应当能使用{模板变量}
代替、{模板变量}
的先后关系应当对应资源名的出现顺序。{模板变量}
包含在花括号里 - 可以使用多个数字或字母,数值的大小并不重要,URL描述仅关心资源出现的先后关系。可以使用 {...}
表示不定个数的模板变量。
http://主机/资源分类/资源目录/资源名/资源ID 使用URL描述语法转换结果就是这样: http://主机/{res}/{category}/{name/{id} 也可以使用 http://主机/{res}/{…} 表示。如果 {...}
出现在最后则可以省略
HTTP://主机/资源分类?资源目录=目录名&资源名=资源名&资源ID=资源ID
使用URL描述语法转换以后: HTTP://主机/{res}?资源目录={category}&资源名={name}&资源ID={id}
可以看到资源名是不是写到参数里都能清晰的展现资源定位,要注意 Web API 并不是浏览器,URL并不会出现在浏览器的地址栏,设计一个友好的 API URL 重要的是编程语言里能不能更好的理解并自动分析转换。 例如aardio
中的 web.rest.client
就按照这种 URL 描述语法自动的将 URL 描述转换为aardio
中的函数对象。
原则上URL不应当包含动词,使用HTTP协议的指令动词表示要执行的操作:
GET: 表示获取资源
POST: 表示新增数据
PUT: 表示替换数据
DELETE: 表示删除数据
PATCH: 表示更新数据
一般的Web服务器因为安全方面的考虑对HEAD、GET、POST之外的请求有所限制, 很多API用POST替代PUT,DELETE的功能,而又要做到URL中不出现动词,就背离了REST的初衷了。
因此建议可选在URL资源定位的最可选性的添加扩展的操作动词,例如:
http://host/group/user/userid/ 使用get读取用户信息
http://host/group/user/userid/password/change 使用扩展的change方法修改用户密码
如果按这种规则实现服务端的API,那么在aardio里用 web.rest.client 调用起来就很方便,示例:
1 | import web.rest.jsonClient; |
为什么不直接在每一个请求里写具体的URL呢?要考虑到实现一个API的扩展库,API服务端的地址可能发生变更,使用上面的方法就可以简单的维护一个声明URL参数即可。
Web API 的URL中不应出现文件后缀名:
例如: http://host/x/y.php 应当在服务器上移动到 http://host/x/y/index.php ,然后提供给客户端的API应隐藏默认的文档名,即 http://host/x/y/ 这样的好处是服务端变更实现会非常方便。
Web API 的URL中不应出现IP地址,即使是测试期间,也应尽可能的使用域名替代IP地址。
]]>示例:
::User32.MessageBox(0,"测试","标题",0)
调用约定在加载DLL的参数中指定,支持cdecl不定个数参数,有很多API根据不同的用法可以传入不同类型的参数, 如果我们在aardio中不是先写一个API声明,而是直接去调用API,这时候就可以根据需要更灵活的改变参数类型。一般建议不要先声明API再去调用 - 直接调用更方便也更节省资源(除非有特殊的数据类型必须通过声明API来指定)。
null参数不可省略
数值参数一律处理为32位int整型,32位整数类型,小于32位的整数、枚举类型、8位或32位bool值都跟int 32位数值兼容,可以直接写在参数里,示例:
32位整型以及小于32位的整型参数都可以直接传入aardio数值。
例如C语言API声明为:void setNumber( short n )
在aardio里如下调用就可以dll.setNumber( 123 )
64位整数(C语言中的long long)可以math.size64对象表示,或者用两个数值参数表示一个64位整数值参数,其中第一个参数表示低32位数值,第二个参数表示高32位数值(一般可以直接写0)。
对于任何数值类型的指针(输出参数)一律使用结构体表示,
例如C语言API声明为:void getNumber( short *n )
在aardio里如下调用就可以
1 | var n = { word value } |
API函数中的数组指针,在aardio
中可以使用结构体指针替代,例如C语言中的 int data[4];
在aardio中写为 {int data[4];}
如果是字节数组指针也可以使用raw.buffer()
函数创建的字节数组替代。
所有结构体一律处理为输出参数并在aardio
返回值中返回,其他类型只能作为输入参数。注意在aardio
中,任何结构体在API调用中传递的都是结构体指针(传址)。
因为没有参数类型声明,调用代码有责任事先检查并保证参数类型正确,传入错误的参数可能导致程序异常。
直接调用API的 返回值默认为int
类型
可以使用[API尾标]
改变返回值为其他类型
未声明的 API函数自身在aardio
中是一个普通的aardio
函数对象,不能作为函数指针参数传给 API参数(声明后的API函数对象是可以的)
当不声明直接调用API时,API函数名尾部如果不是大写字符,则可以使用一个大写的特定字符(API尾标)修改默认的API调用规则,在API函数名后添加尾标,不会影响到查找API函数的结果,无论真实的API带不带指定的尾标 - aardio都能找到真实的函数。 所有可用的[API尾标]如下(函数名的最后一个特定字符是尾标):
dll.ApiNameW()
切换到Unicode
版本,字符串UTF8
<->UTF16
双向转换dll.ApiNameA()
切换到ANSI
版本,字符串不作任何转换dll.ApiNameL()
返回值为64位LONG
类型dll.ApiNameP()
返回值为指针类型dll.ApiNameD()
返回值为double
浮点数dll.ApiNameF()
返回值为float
浮点数dll.ApiNameB()
返回值为 C++ 中的8位bool
类型->
字符串一般直接转换为字符串指针,buffer
类型字节数组也可以作为字符串指针使用,如果API需要向字符串指向的内存中写入数据,那么必须使用raw.buffer()
函数创建定长的字节数组。普通的aardio
字符串指向的内存是禁止写入的(aardio
中修改普通字符串会返回新的字符串对象,而不是在原内存上修改数据)
对于非Unicode
API字符串直接输入原始的数据(对于文本就是UTF8编码),对于声明为Unicode
版本的API,字符串会被强制转换为Unicode(UTF16)
,但buffer
类型的参数仍然会以二进制方式使用原始数据与API交互(不会做文本编码转换)
可以在 raw.loadDll()
加载 DLL时在调用约定中添加,unicode
声明一个 DLL默认使用Unicode API
。
也可以在函数名后添加尾标 W
声明一个Unicode API
, 即使真实的API函数名后面并没有 W
尾标,你仍然可以添加 W
尾标调用 API。aardio
在找不到该 API函数时,会移除 W
尾标,并且认为该 API函数是一个Unicode API
,注意 W
必须大写并紧跟在小写字母后面。
直接调用 API时,如果目标 API函数并不存在,而是存在加 W
尾标的Unicode API
,aardio
将会自动切换到Unicode API
,并在调用函数时,自动将aardio
的UTF8
编码转换为 API所需要的UTF16
编码。
反之,在API函数名后也可以显式的添加 A
尾标强制声明此 API是一个 ANSI
版本的函数(对字符串参数不使用任何 Unicode
转换,即使加载 DLL时在调用约定中声明了默认以 unicode
方式调用),规则同上 - 也即真实的API函数名后面有没有 A
尾标并不重要,在aardio
中都可以加上 A
尾标。
一些API在接收字符串、字节数组等参数时,通常下一个参数需要指定内存长度, aardio
中用#
操作符取字符串、缓冲区的长度时,返回的都是字节长度,一些 API可能需要你传入字符个数, 发果是Unicode
版本的 API一个字符为两个字节,对于一个UTF8
字符串应当事用string.len()
函数得到真正的字符长度, 而Unicode
字符串则用#
取到字节长度后乘以2即可。
例如 chromeDriver.exe ( 不同版本的chrome要下载不同版本的 chromeDriver.exe )。其他的东西我们就不需要了,安装这个安装那个多麻烦对吗?!
看一下其他语言的封装库,代码可能很多,但是不要被吓倒了,用 aardio 我们真的只要几句代码就可以实现 WebDriver 客户端了。
1 | import web.rest.jsonClient; |
现在用aardio 最新版中提供的 chrome.driver 所有麻烦都可以解决了,
chrome.driver 会自动查找Chrome的安装位置、版本号,自动匹配最合适的ChromeDriver版本,并且负责自动下载安装,自动分配空闲端口,所有事情全自动准备好,只要运行下面的代码就可以了。
现在看代码,用法非常简单:
1 | //WebDriver自动化 |
下面的问题在新版中已解决,可忽略:
注意 Chrome新版会强制显示控制台( 隐藏也会强行弹出黑窗口,旧版可以隐藏这个黑窗口 ),
如果想隐藏黑窗口,那么可以用旧版Chrome,在创建 chrome.driver对象时可以在参数中自定义chrome.exe的路径。
也可以通过 ChromeDriver 调用 Electron,几句代码就可以了:
1 | import electron.driver; |
1 | import chrome.driver; |
1 | import chrome.driver; |
也可以下面这样写:
1 | import chrome.driver; |
chrome.driver新版功能演示,操作chrome就像直接执行Javascript函数那么简单
1 | import chrome.driver; |
网上一些讨论认为这个问题无解,WebDriver也没有找到相关参数,
直觉这个可能在启动参数里打开控制台,于是我写了一个假的 chrome.exe,再用 ChromeDriver.exe 调用他,代码如下:
1 | import console; |
chrome.exe获得的启动参数如下:
1 | --disable-background-networking --disable-client-side-phishing-detection --disable-default-apps --disable-hang-monitor --disable-popup-blocking --disable-prompt-on-repost --disable-sync --disable-web-resources --enable-automation --enable-logging --force-fieldtrials=SiteIsolationExtensions/Control --ignore-certificate-errors --load-extension="C:\Users\***\AppData\Local\Temp\***\internal" --log-level=0 --metrics-recording-only --no-first-run --password-store=basic --remote-debugging-port=0 --test-type=webdriver --use-mock-keychain --user-data-dir="C:\Users\***\AppData\Local\Temp\***" data:, |
我们看到可疑参数--enable-logging
,
进一步测试发现:排除这个参数就可以关闭新版chrome启动跳出来的控制台窗口了,示例代码:
1 | import chrome.driver; |
已更新 chrome.driver 默认禁用控制台窗口,
但仍然可以使用
driver.addArguments("--enable-logging")
启用这个参数。
aardio新版经过大力改进,
现在 chrome.app, chrome.driver 已经可以相互结合使用,chrome与aardio交互更加简单方便。
下面是一个简单的例子:
1 | import chrome.app; |
方法一:
1 | import chrome.driver; |
虽然不显示上面的提示了,但是弹出一个更大的警告。
方法二:
1 | import chrome.driver; |
不显示上面的提示,也没有警告了,但是可以看到提示框显示然后快速的关掉,会闪烁一下。
方法三:
1 | import chrome.driver; |
用–app模式的方法完美,地址栏、提示框、警告都去掉了,
但是有一个奇怪的事情是,启动网址要写成 http://www.so.com/index.html 这样,如果不写 index.html 有时候会白屏,但不是每个网站都这样。
Chrome每个进程只能绑定单独的用户目录 - 才能创建单独的远程调试端口,
ChromeDriver 的办法是每次都创建一个临时的用户目录,然后每次都创建新的临时用户目录,而且又不负责删除(其实可以设置为重启系统自动删除,不知道Chrome为什么没有这么做),所以我们只好自己清理了,代码如下:
1 | import console; |
Typora是我非常喜欢的Markdown编辑器,之前的一个更新,Typora支持了“上传图像”的功能(即写文章时,插入图片自动将其上传至图床),我们可以直接借助IPic、uPic、PicGo等程序,配合Typora自动将图片上传至又拍云等对象存储平台。但是,官方的文档晦涩难懂,我尝试进行了一些配置,但始终还是不好使,那咋办嘛?
其实,将本地图片上传至图床,这个过程本质上来说就是一条HTTP请求,但是如果这个过程还需要在后台一直开着一个图床软件或者安装一些命令行工具(更何况很多工具也是收费的),代价就有些大了。庆幸的是,Typora支持不借助这些图床工具,通过自定义命令(脚本)的方式,完成自动上传图片的功能。
那么,能不能自己写一个十几行代码的脚本来适配Typora呢?当然可以了。
以Typora自定义命令上传图片为例,当我们从我们插入一张图片时,发生了什么?我们在Typora插入一张本地的图片后,Typora会调用预先设置的自定义命令(通常是运行一个脚本)来上传图片,自定义命令(脚本)上传完图片后输出(注意这里有坑)相应的URL,Typora会读取该URL,并自动把本地图片地址替换为相应的URL。所以只要配置得当,我们在写作时,只需要准备好素材,直接对素材command + c
、command + v
了。
如果我们要通过自定义命令来上传图片,只需要三步:
脚本如下所示:
1 | @echo off |
我们在写一个脚本的时候,大体上关注三部分内容,输入、处理过程和输出。处理过程是自己实现的所以还好,但Typora上传图片自定义命令的传参和输出是真的奇坑。
传参
。Typora上传图片调用自定义命令时,会将待上传的图片作为命令行参数,传入脚本。如bash upload.sh 1.jpg 2.jpg
,这里具体有几张待上传的图片不确定,所以参数长度是不固定的,你的脚本必须上传所有作为参数传入的图片。Typora并没有在文档中直接说明这一点(传参的方式、长度),真的给我坑坏了。输出
。事实上,Typora并不关心我们脚本中上传图片的具体过程如何,它只关心我们脚本的最终输出。Typora要求脚本输出结果的方式简单粗暴,直接echo
(其他语言比如Python的print
)。注意,直接echo
也是有格式要求的,脚本需要首先输出Upload Success:
,之后,一行对应一个URL,具体格式如下:1 | Upload Success: |
这里,我使用bat脚本,事实上,你可以使用任何变成语言,上传图片至任意的平台。只要你脚本处理好传入的参数,上传完所有图片,最终的输出结果是上面的格式即可。
Typora上传脚本支持的自定义命令,可以在偏好设置中选择。上传服务选择Custom Command
,自定义命令就是我们插入图片后,Typora调用的命令。如刚才我们的脚本名称为upload.cmd
,自定义命令就可以设为upload
,注意替换upload
的路径为绝对路径。
完成脚本和偏好设置后,就可以测试脚本是否正常了。打开偏好设置,直接点击
验证图片上传选项
。
市面上确实有很多功能丰富的Markdown编辑器,但所见即所得、小巧轻量的Typora依然是我最喜欢的。图片上传功能的加入,极大方便了写作的过程,想想之前,写文章需要手动将图片拖到图床APP,再把URL复制到文章中,就两个字,“繁琐”。
最后,用Typora写作,自定义命令上传图片,自己写一个脚本,四行代码,卸载掉各种图床工具,插入图片直接command + v
,写作原本就该这么简单嘛!©
simpleHttpServer
库实现 python 例程中的 HTTP 服务器功能!1 | //简单服务器示例 |
注意:本文针对Windows平台和Hexo 3.2.2
1 | $ hexo -v |
主要使用 git bash,如果对 git 命令不熟悉的也可以使用 git 客户端进行某些操作
Github For Windows
因为要使用 npm,比较简单的方法就是安装 node.js
安装完成后添加 Path 环境变量,使 npm 命令生效
;C:\Program Files\nodejs\node_modules\npm
没有Github 账号的话,需要注册一个,然后创建一个仓库,名字是[yourGithubAccount].github.io
使用 git bash
生成 public ssh key
, 以下是最简单的方法
1 | $ ssh-keygen -t rsa |
然后在 C:\Users\[用户名]\.ssh
目录下会生成 id_rsa.pub
,将内容完全复制到 Github Account Setting
里的 ssh key
粘贴即可。
测试
1 | $ ssh -T git@github.com |
设置用户信息
1 | $ git config --global user.name "[yourName]"//用户名 |
经过以上步骤,本机已成功连接到 github,为部署打下基础。
创建本地目录,然后使用 git bash 或者客户端 clone 之前创建的仓库[yourGithubAccount].github.io
进入仓库目录,使用 npm
安装配置 hexo
1 | $ npm install -g hexo-cli |
安装 Hexo 插件
1 | npm install hexo-generator-index --save |
安装
ejs
, 否则无法解析模板
1 | $ npm install ejs |
安装 hexo 所需的依赖模块1
npm install
然后运行下面的命令生成 public 文件夹
1 | $ hexo g |
在浏览器输入
localhost:4000
本地查看效果
hexo 有很多主题可选,我选了 indigo,Material Design 风格的Hexo主题,基于 Hexo 3.0+ 制作。支持多说评论、网站统计、分享等功能,只要稍微配置即可使用。可以根据自己需求进行选择。
配置 _config.yml
1 | deploy: |
1 | $ hexo d |
即可将 hexo 部署到 github 上
提示找不到 git 时
需执行(虽然之前已经执行过)
1 | npm install hexo-deployer-git --save |
然后
1 | $ hexo d |
即可访问:1
http://[yourGithubAccount].github.io/
1 | $ hexo new "title" |
然后在 source/_post
下会生成该.md文件,即可使用编辑器编写了编写过程中,可以在本地实时查看效果,很是方便。支持 markdown
,不了解的自行 百度 。
编写完成后,部署还是一样的
1 | $ hexo d -g |
如果部署过程中报错,可执行以下命令重新部署
1 | $ hexo clean |
1 | $ hexo new page "about" |
该命令会生成 source/about/index.md
,编辑即可
1 | $ npm update |
1 | npm update -g hexo |
在 /source/
目录下新建内容为自定义域名的 CNAME
文件,部署即可(域名设置略)
备注:Hexo简写命令
1 | hexo n #new |
1 | $ hexo new "My New Post" |
更多信息: Writing
1 | $ hexo server |
更多信息: Server
1 | $ hexo generate |
更多信息: Generating
1 | $ hexo deploy |
更多信息: Deployment
]]>