Ben's Homepage

君子善假于物


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

WWDC2016之初识Xcode Source Editor Extension

发表于 2016-06-23   |   分类于 iOS开发   |  

以往Xcode插件开发在没有官方支持的情况下,基本处于处于刀耕火种的状态。虽然后来有了Xcode-Plugin-Template和各种dump好的头文件,我们仍然需要在没有文档的情况下做各种猜测和hook。关于插件的开发,可以看看这篇文章。幸运的是苹果终于注意到了这一需求,并在WWDC2016上发布这一新特性–Xcode Source Editor Extension。

简介

Xcode Source Editor Extension,从字面意思理解就是Xcode源代码编辑器的一个扩展,事实上它的命名的确很好地反映了它的功能和定位。那么基于这项技术我们究竟能做什么呢?WWDC session 414给我们的答案是:

Add commands to the source editor

Edit text

Change selections

给Xcode的代码编辑器扩展一些命令(在Editor菜单下增加额外的菜单),通过这些命令对源代码进行编辑(包括整个文件和选中的部分,这里的selection我起初不理解,后面做Demo就会理解了)。

相比于以往的插件开发,Xcode Source Editor Extension具有以下特点:

  • 稳定。extension运行在其独立的进程中,插件的崩溃不会再引起Xcode的崩溃了。
  • 安全。extension拥有自己的sandBox,而且不会直接去操作Xcode工程,而是由Xcode将text以invocation的方式传递。
  • 快速。每个extension在Xcode启动后会即会被异步地以XPC的方式加载。
  • 简单。苹果提供了官方支持。

此外,作为App Extension的一种,可在App Store发布,优秀的插件也可以为开发者增加收入。下面我们就跟着demo来探究如何实现一个Xcode Source Editor Extension。

工程创建

我将创建一个为全部选中行添加注释的demo。根据官方指导,创建一个插件工程非常容易。首先我们创建一个Cocoa Application命名为”XTExtension”,接着在Application Extension中选中Xcode Source Editor Extension,命名为XTComment。
extension template
此时工程的XTComment目录下有两个文件SourceEditorExtension.swift和SourceEditorCommand.swfit,SourceEditorExtension用来管理extension的生命周期相关,SourceEditorCommand则用来处理具体的命令。Extension的加载和处理流程如下:
flow
Xcode的启动时候会记载所有的extension,并保证它们的生命周期。当用户点击选择命令按钮后,Xcode会以invocation的形式通知到extension,extension做出相应的处理并把处理结果回调给Xcode。SourceEditorExtension.swift的方法extensionDidFinishLaunching在extension被加载时调用,可以在这里做一些初始化的工作。

1
2
3
4
func extensionDidFinishLaunching() {
// If your extension needs to do any work at launch, implement this optional method.
print("Extension launched...")
}

添加菜单

打开Info.plist,展开NSExtension至XCSourceEditorCommandDefinitions,命令菜单的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<key>NSExtensionAttributes</key>
<dict>
<key>XCSourceEditorCommandDefinitions</key>
<array>
<dict>
<key>XCSourceEditorCommandClassName</key>
<string>$(PRODUCT_MODULE_NAME).SourceEditorCommand</string>
<key>XCSourceEditorCommandIdentifier</key>
<string>com.xt.APPlugins.AutoComment.SourceEditorCommand</string>
<key>XCSourceEditorCommandName</key>
<string>Source Editor Command</string>
</dict>
</array>
<key>XCSourceEditorExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SourceEditorExtension</string>
</dict>

其中:

  • XCSourceEditorCommandClassName指向命令的处理类,该类实现XCSourceEditorCommand。
  • XCSourceEditorCommandIdentifier是命令的唯一标识,通常我们会对一组命令设置同一个XCSourceEditorCommand,在invocation中获取此标识做区分处理。
  • XCSourceEditorCommandName是命令在菜单栏展示的菜单名称。

此外,菜单项是动态加载的,可以实现SourceEditorExtension的commandDefinitions:实现,但是这会覆盖掉Info.plist中的定义。

1
2
3
4
5
6
var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: AnyObject]] {
// If your extension needs to return a collection of command definitions that differs from those in its Info.plist, implement this optional property getter.
return [[.classNameKey: "SourceEditorCommand",
.identifierKey: "CustomIdentifier",
.nameKey: "CustomeName"]]
}

Extension的菜单会默认加载在Show Code Coverage下。编译运行,选取Xcode-Beta作为Lanuching App,点击Editor,然而Show Code Coverage下空空如也,什么也没有。作为新主题,可查的资料实在太少,一度以为因为Xcode是Beta版的问题并打算放弃了。很偶然的一个机会在Xcode 8.0 beta Release Notes看到了这个问题的答案,WTF!

To use Xcode Extensions on macOS 10.11 El Capitan, you must first launch Xcode and let it install additional system components. Then, in Terminal, run sudo /usr/libexec/xpccachectl.
Once the operation completes, restart your Mac. (26106213)

业务实现

打开SourceEditorCommand.swift,实现perform(with:completionHandler:)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (NSError?) -> Void ) -> Void {
var lineIndexes = [Int]()
for range in invocation.buffer.selections {
guard let range = range as? XCSourceTextRange else {
continue
}
let start = range.start
let end = range.end
for i in start.line...end.line {
lineIndexes.append(i)
}
}
lineIndexes.reverse()
for lineNumber in lineIndexes {
guard let line = invocation.buffer.lines[lineNumber] as? NSString else {
continue
}
let commentLine = NSString(format: "// %@", line)
invocation.buffer.lines.insert(commentLine, at: lineNumber)
}
// Implement your command here, invoking the completion handler when done. Pass it nil on success, and an NSError on failure.
completionHandler(nil)
}

invocation的buffer提供了比较丰富的信息,包括文件的lines, selections, indentationWidth等等,具体作用可以查看WWDC视频或者苹果的文档查看,在此就不一一介绍了。
feature show
此外,需要注意的一点是,苹果希望文本的编辑是快速流畅的,如果你的处理的文本时间过长苹果就会为用户提供一个取消的入口,这会触发invocation的cancellationHandler,所以在这里终止你的耗时的工作,否则你的extension可能不能正常地工作。最后,附上苹果的extension开发建议。

Start up quickly

Use GCD and follow standard asynchronous patterns

Don’t replace the whole buffer if you don’t have to

Handle cancellation quickly

小结

Xcode Source Editor Extension提供了一种官方Xcode插件开发方法,从上文可以看出,其所涉及的类和函数都很少,非常容易上手。同时,从目前来看,她的能力还相对较弱,一些复杂的如FuzzyAutocompletePlugin还无法实现。但我相信很快会有优秀的插件来丰富Xcode功能和提升开发效率的。

Demo

参考

  • WWDC session 414
  • xTextHandler

查找Xcode工程未使用的图片资源

发表于 2016-03-17   |   分类于 iOS开发   |  

背景

随着APP的功能越来越丰富,安装包的尺寸也在快速增长。像16G这种在往日非常大的存储空间,现在也越来越捉襟见肘,安装包的大小已经成为衡量APP质量的指标之一。分析iOS的安装包文件.ipa文件我们可以看到,图片资源在安装包中还是占了比较大的比重,通过减少图片的总尺寸可以达到我们的目的。方法有两个,一是压缩单张图片的尺寸,比较著名的有imageOptim,二是减少图片的数量。实际上在我们的开发过程中,由于业务和设计的快速变化,导致工程中有许多冗余的图片资源。我们的目标就是找出并删除这些冗余的图片。为此,开发了工具UnusedImagesFinder。

使用方法

使用方法自然是相当简单的,打开你的Terminal,执行以下命令:

1
python unused_images_finder.py PROJECT_PATH {SOURCE_PATH}

其中PROJECT_PATH是你的工程文件的路径,也就是.xcodeproj文件的路径。SOURCE_PATH是一个可选参数,一般设置为工程的根目录,默认为.xcodeproj文件的上级目录。检查的结果写入到当前目录下的result.txt文件中。按照上述的步骤执行,如果在result.txt文件中发现一堆误杀的文件,这时你就需要注意了,可能你的代码不是以常规的imageNamed:@"xxx"的形式引用的。此时,我们需要修改我们的源文件了,打开ununsed_images_finder找到174行,看到如下的代码:

1
imageReferencePatterns = [re.compile(r".*imageNamed:@\"([^@\.]+)(@\dx)*(\.png|\.jpg)*\".*")]

根据工程中引用图片的方式可以定制正则表达式,加入数组或者替换掉即可。

实现思路

大致的实现思路如下:

  • 找出图片资源

    在使用脚本中有一个参数是必填的,这个参数就是.xcodeproj,为什么一定要这个参数呢?因为在它的目录里有一个重要的文件project.pbxproj,这个文件包含了工程所有的构建信息,包括图片资源和编译源文件。打开project.pbxproj,找到Begin PBXResourcesBuildPhase section和End PBXResourcesBuildPhase section之间的内容,这里就是工程所引用的资源,这里有一些plist,还有我们想要找的图片文件。通过正则匹配,我们可以拿到图片的名称。最初的版本,我就是这么写的,运行结果出来后,发现有非常多的漏查。查找后发现,工程中部分图片被放到.bundle文件中,其中的文件并不会出现在project.pbxproj,类似的还有image.xcasset中的图片资源。所以在这里改变策略,遍历整个工程,找出所有图片。

  • 找出源文件

    有了图片的查找经验,找资源文件就很简单了,所有被编译的资源都在project.pbxproj里的Begin PBXBuildFile section和End PBXBuildFile section之间。为什么我们不是在遍历文件时找出所有源文件呢?因为我们有许多不好的习惯,很多时候没用的源文件只是简单地被从工程引用中移除,并没有被物理删除。如果我们把这部分文件也加进来,它们引用的图片将成为漏网之鱼。

  • 扫描未引用的图片资源

    有了上面两步的工作,第三步就比较简单了。遍历项目中的源文件,通过正则匹配的方式找到已引用的图片(注意,正则可能需要使用者补充,见使用方法),剩下的自然就是未被引用的资源了。

已知问题和改进

当前存在的不足如下:

  • 目前不支持Storyboard和xib
  • 引用如果没有直接使用文件名,无法检测出,比如

    NSString *imageName = @"imageName";
    UIImage *image = [UIImage imageNamed:imageName];
    ...
    UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"xxx%d", 2]]
    
  • 误杀

    • Icon, Default 等文件不会直接被代码引用,查找的时候会被误杀
    • 问题二的文件同样会被误杀

未来改进的方向:

  • 改进上面的问题
  • 脚本集成到持续集成中
  • 提高检测率

最最最重要的一点,删除图片之前做好确认工作!

@IBDesignable-所见即所得的Custom View

发表于 2015-05-20   |   分类于 iOS开发   |  

iOS8发布后,XCode对自定义控件支持有所加强,之前在途牛旅游客户端开发了一个通用的控件TNStepper,这篇文章主要通过升级TNStepper来简单介绍这项新技术@IBDesignable @IBInspectable。

问题

在iOS的开发中,对于一些具备独立功能的可复用的控件我们往往会把它做成一个自定的控件(custom view)。使用控件时,我们调用initWithFrame:方法创建控件,对于storyboard/xib,我们通过在storyboard/xib拖拽一个UIView控件(要求自定义控件实现initWithCoder:方法),并将其Class指定为MyCustomView。我们看到的一个空白的view,并不是我们设计好的custom view,根本没有发挥storyboard/xib所见即所得优势,也没法动态去看到custom view属性调整后的效果。

@IBDesignable实时渲染

本文使用Swift编写了TNStepper的升级版VisualStepper,通过该控件来简单介绍这项新技术,读者可先行下载代码。其中,XTStepper为我们的自定义View。

首先简单介绍一下XTStepper的代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}

required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}

func xibSetup() {
view = loadViewFromNib()
// Add constraint here
...
self.updateApperance()
}

func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "XTStepper", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}

作为一个重度的storyboard/xib使用者,我是不希望自己去手写创建代码的,控件里的子view都是XTStepper.xib创建的,通过func loadViewFromNib() -> UIView加载。这样做的好处就是所见即所得,而且我们不用写那些繁琐的constraint。XTStepper.xib长这个样子。

image01

打开Main.storyboard文件,却发现一片空白,完全看不到我们设计好的控件的样子。
我们所需要做的很简单,只要在我们希望实时渲染的自定义控件前加上@IBDesignable关键字即可。

1
2
3
4
5
import UIKit

@IBDesignable class XTStepper: UIControl {
...
}

现在让我们来见证一下它的魔力。

image02

虽然只有加一个关键字这么简单,我们仍然要注意,因为从storyboard创建ViewController调用的是这个初始化方法,我们一定要覆写init(coder aDecoder: NSCoder),做初始化工作。现在控件已经是我们所希望看到的样子了,但这还不够。UIStepper是可以动态调整相关的属性,来控制控件的状态的,我们也想要VisulStepper一样好用。

@IBInspectable动态更新

我们为VisualStepper添加三个类似于TNStepper的属性,当前值value、maxinumValue和minmumValue,当这几个值发生变化的时候,控件的+``-的可用状态会发生变化。

1
2
3
4
5
func updateApperance(){
valueLabel.text = String(value)
minusButton.enabled = value > minmumValue && value <= maxinumValue
plusButton.enabled = value >= minmumValue && value < maxinumValue
}

我们希望可以在InterfaceBuilder里设置这些属性,方法很简单,在属性前加上@IBInspectable。我们可以看到刚才定义的几个属性,在这里我们可以设置他们的值。
在设置的时候,我们发现控件的状态不会变化,即使value <= minumValue了,-仍然是可以点击的状态,因此我们需要在检测到值变化的时候,去更新我们控件的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@IBInspectable var value:Int = 0 {
didSet{
sendActionsForControlEvents(UIControlEvents.ValueChanged)
self.updateApperance()
}
}
@IBInspectable var maxinumValue:Int = 0 {
didSet{
self.updateApperance()
}
}
@IBInspectable var minmumValue:Int = 0 {
didSet{
self.updateApperance()
}
}

至此我们完成了一个更好用的更像UIStepper的自定义Stepper。

参考文献

[1] 王巍:WWDC 2014 Session笔记 - 可视化开发,IB 的新时代

工欲善其事,必先利其器-我的XCode插件

发表于 2015-01-05   |   分类于 iOS开发   |  

XCode为我们提供了一个相对完善的集成开发环境(IDE),但它仍有诸多不便,在很多地方做的不是那么好,所幸Apple提供了插件编写功能。通过使用多种插件,可以显著提高我们的开发效率。

KSImageNamed

你是否曾为[UIImage imageNamed:]无法准确拼出图片名称感到烦恼?是否也曾反复把图片名拷贝到代码里?是否遇到过把图片名大小写搞错而导致真机不能正常显示图片资源?那么这款神器绝对适合你,而且是完全免费的哟。经博主测试,用了KSImageNamed,手不酸了,眼不疼了,一口气写100行。

Screenshot

VVDocumenter

喵神出品,必属精品。iOS开发大神onevcat(人称喵神)倾情奉献,分分钟生成API文档,与Apple Documention无缝对接,实乃SDK开发、底层API开发人员居家旅行之利器,你还在等什么!

Screenshot

Peckham

拷贝代码忘记import头文件?代码写到上千行引入新类需要导入新类,cmd+shift+f找到这个类,拷贝头文件,然后回到原文件,滚动到顶部再导入。Peckham统统帮你搞定,control+cmd+h一键搞定(靠,明明是三键好不好),麻麻再也不用担心我导入头文件。

Peckham.gif

Cocoapods-UI

不要问我为什么不用酷酷的命令行,因为我懒,Cocoapods的UI工具拿去不谢。

Menu

使用CALayer的mask动画

发表于 2014-11-17   |   分类于 iOS开发   |  

Core Animation一直是iOS比较有意思的一个主题,使用Core Animation可以实现非常平滑的炫酷动画。Core animtion的API是较高级的封装,使用便捷,使得我们免于自己使用OpenGL实现动画。本文主要介绍如何使用CALayer的mask实现一个双向注水动画(姑且这么叫吧)。

animation gif

了解CALayer的mask

1
2
3
4
5
6
7
8
9
10
/* A layer whose alpha channel is used as a mask to select between the
* layer's background and the result of compositing the layer's
* contents with its filtered background. Defaults to nil. When used as
* a mask the layer's `compositingFilter' and `backgroundFilters'
* properties are ignored. When setting the mask to a new layer, the
* new layer must have a nil superlayer, otherwise the behavior is
* undefined. Nested masks (mask layers with their own masks) are
* unsupported. */


@property(strong) CALayer *mask;

以上是CALayer的头文件关于mask的说明,mask实际上layer内容的一个遮罩,如果我们把mask是透明的,实际看到的layer是完全透明的,也就是说只有mask的内容不透明的部分和layer叠加的部分才会显示出来,效果如下:

Layer mask

实现思路

设计的思路参考《基于Core Animation的KTV歌词视图的平滑实现》,Facebook Shimmer。

flow
在View上重叠放置两个UIImageView: grayHead&greenHead,默认greenHead会遮挡住grayHead。为greenHead设置一个mask,这个mask不是普通的mask,它由两个subLayer:maskLayerUp``maskLayerDown组成。默认情况下,subLayer都显示在mask内容之外,此时mask实际上透明的,由此greenHead也是透明的。现在我们希望greenHead从左上角和右下角慢慢显示内容,那么我们只需要从两个方向为greenHead填充内容就可以了。

###代码片段

  • 创建mask
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
- (CALayer *)greenHeadMaskLayer
{
CALayer *mask = [CALayer layer];
mask.frame = self.greenHead.bounds;

self.maskLayerUp = [CAShapeLayer layer];
self.maskLayerUp.bounds = CGRectMake(0, 0, 30.0f, 30.0f);
self.maskLayerUp.fillColor = [UIColor greenColor].CGColor; // Any color but clear will be OK
self.maskLayerUp.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(15.0f, 15.0f)
radius:15.0f
startAngle:0
endAngle:2*M_PI
clockwise:YES].CGPath;
self.maskLayerUp.opacity = 0.8f;
self.maskLayerUp.position = CGPointMake(-5.0f, -5.0f);
[mask addSublayer:self.maskLayerUp];

self.maskLayerDown = [CAShapeLayer layer];
self.maskLayerDown.bounds = CGRectMake(0, 0, 30.0f, 30.0f);
self.maskLayerDown.fillColor = [UIColor greenColor].CGColor; // Any color but clear will be OK
self.maskLayerDown.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(15.0f, 15.0f)
radius:15.0f
startAngle:0
endAngle:2*M_PI
clockwise:YES].CGPath;
self.maskLayerDown.position = CGPointMake(35.0f, 35.0f);
[mask addSublayer:self.maskLayerDown];

return mask;
}
  • 做动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)startGreenHeadAnimation
{
CABasicAnimation *downAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
downAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(-5.0f, -5.0f)];
downAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(10.0f, 10.0f)];
downAnimation.duration = duration;
[self.maskLayerUp addAnimation:downAnimation forKey:@"downAnimation"];

CABasicAnimation *upAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
upAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(35.0f, 35.0f)];
upAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(20.0f, 20.0f)];
upAnimation.duration = duration;
[self.maskLayerDown addAnimation:upAnimation forKey:@"upAnimation"];
}

完整代码

小结

CALayer提供另外一种操作UI的手段,虽然它提供的API比UIView较底层,但它能提供更加丰富的功能和更高的性能(CALayer的动画是在专门的线程渲染的)。涉及到复杂且性能要求高的UI界面,CALayer的作用就比较明显了,比如AsyncDisplayKit。通过本片文章,我们其实也能看出CALayer的一个用处,通常我们处理圆角时会直接去修改CALayer的cornerRadius,但这种做法性能比较差,尤其是放在列表里的时候,现在我们有了mask,这样我们可以直接改变layer的mask,而不会影响到图形渲染的性能。(这里是严重错误的)

用Octopress写博客

发表于 2014-11-12   |   分类于 非技术类   |  

从昨天晚上折腾到现在终于把Octopress的环境搭好了,网上有很多搭建环境的例子,无奈有些说的不是很明确,东拼西凑了一些教程,这些都非本人原创,把我的经历给大家分享下,让大家少走弯路。

最近产生了写博客的想法,一方面想把自己的开发经历和技术经验分享出来,另一方面也希望在写作的过程中能加深自己的了解。个人比较喜欢喵神和唐巧的技术博客的风格,喵神的Blog模板没有尝试,主要参考唐巧的文章,完成了本文的写作环境的搭建。

搭建

搭建环境主要参考这里http://blog.devtang.com/blog/2012/02/10/setup-blog-based-on-github/,大家直接跳过去看就好了。主要说说我的经验,ruby的环境不需要1.9.3的,Yosemite默认的2.0是Ok的,所以没必要纠结地去装gcc 4.2,直接装Octopress就行。gem install bundler相当地慢,曾经就因为这个放弃了一次,这个没被墙,就是慢,耐心等一下。

文章里漏掉的一点就是设置octopress与github-pages的连接,第一次发布,在执行rake deploy之前,要先执行rake setup_github_pages,在输入提示中输入你的github-pages地址,比如我的git@github.com:wuwen1030/wuwen1030.github.io.git。大家在配置好,最好就不要手动去改github-pages的内容啦,这可能会导致发布失败噢,原因就是octopress的发布内容和github服务端的内容不一致造成的,deploy 时明明看到 git pull… 了,没搞懂,大家如果知道请指教一下。

deploy之后,发现和大神的Blog还是很不一样嘛,不知道从哪儿窜出来的各种英文,现在需要配置一下,定制自己的模板啦。

配置

唐巧的文章配置这块写的不是太细,谷歌,DuckDuckGo被各种屏蔽,只好求助度娘,配置主要是参考http://www.cnblogs.com/oec2003/archive/2013/05/31/3109577.html。大家自行阅读吧,写的很细了。配置这块还有很多可研究的,待以后深入研究再补充上来。

至此,你已经得到了一个看起来很酷的blog模板了,可以拿出来装一装了(Octopress号称就是给Hacker用的么),唯一的问题就是访问速度很慢,不过没关系,已经有解决方案了,拿来主义的博主再次发出链接大招http://blog.devtang.com/blog/2014/06/02/use-gitcafe-to-host-blog/,一步步按照文章里的来,像我这样的小白也能搞好。感谢方校长的GFW成功阻击境外反动势力对我国的渗透,即使切到GitCafe你的Blog访问速度依然很慢,查看你主页的源码,你会看到这个:

1
2
3
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<link href="//fonts.googleapis.com/css?family=PT+Serif:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=PT+Sans:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css">

问题就在这里,替换掉Google的jquery和font链接,打开/source/index.html,做如下替换:

1
2
<!-- 替换jquery ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js-->
<script src="http://ajax.useso.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>

打开 source/_includes/custom/head.html,替换成如下代码:

1
2
3
<!--Fonts from Google"s Web font directory at http://google.com/webfonts -->
<link href="http://fonts.useso.com/css?family=PT+Serif:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css">
<link href="http://fonts.useso.com/css?family=PT+Sans:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css">

后话

完成这些之后,你就可以像博主一样使用优雅的Markdown在这写作,推荐一个不错的Mac开源Markdown编辑工具。

BenXia

BenXia

6 日志
2 分类
GitHub
© 2016 BenXia
由 Hexo 强力驱动
主题 - NexT.Mist