如何构造 Swift 框架(四)

本贴最后更新于 2868 天前,其中的信息可能已经水流花落

上期提到了使用Moya作为网络基础模块,但是涉及到了一个sampleData的问题,我们也是即时的提交了一个issue来质问这样的默认Response data为什么类型竟然是Optional的。Moya的开发者举例:可以将上一次获取到的数据在需要的时候(网络请求失败)传入这里,所以进而给出建议:将var sampleData改为var cachePolicy进行缓存控制即可,缓存过期的时间由Server端使用Cache-control或Expires决定,目前有的回复是,作者觉得这个建议很棒,说不定有机会为Moya加入缓存机制。接下来继续我们的开发计划:

推送服务

应当明确的是,每家公司用的推送第三方都是不同的(大部分是阿里云、极光、个推),所以继承第三方SDK这个事情不应该出现在框架中。框架仅仅负责申请推送能力即可。测试:在测试之前,Info.plist中所需要申请权限的Key需要自己手动配置。Xcode 8 后打开推送需要在程序中打开选项:

这样,如果是单单写权限的话,直接用之前我们引入的PermissionScope就可以搞定了,Push的class可以写为open的,因为每个项目对Push的需求不同,所以在Push中我们顺便截获一下信息然后提供给用户,也很简单。所以获得推送权限的需求我们放到Permission.swift中。由于屏幕限制,所以Permission也最多允许大家同时打开3个权限。改写之前的Permission.swift

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
import Foundation
import PermissionScope
 
public enum INSPermissionType {
case notification(Set<UIUserNotificationCategory>?, String)
case locationAlways(String)
case locationWhenInUse(String)
case contact(String)
case event(String)
case microphone(String)
case camera(String)
case photos(String)
case reminders(String)
case bluetooth(String)
case motion(String)
}
 
open class Permission {
open static let `default` = Permission()
static let pscope: PermissionScope = {
let permissionScope = PermissionScope()
// Default customs
permissionScope.headerLabel.text = "嗨,你好!"
permissionScope.bodyLabel.text = "在使用我们的应用之前\n我们需要你做一些事情:"
permissionScope.closeButtonTextColor = UIColor.clear
permissionScope.permissionButtonΒorderWidth = 0.5
permissionScope.permissionButtonCornerRadius = 2
/// 如果你希望更改权限开启按钮的英文,就需要自己配置本地化文件
/// 参考这里 https://github.com/nickoneill/PermissionScope/pull/12#issuecomment-96428580
return permissionScope
}()
 
open class func requestPermission(_ permissionTypes: [INSPermissionType], _ authChange: authClosureType? = nil, cancelled: cancelClosureType? = nil) {
for item in permissionTypes {
switch item {
case .notification(let categories, let message):
pscope.addPermission(NotificationsPermission(notificationCategories: categories), message: message)
continue
case .locationAlways(let message):
pscope.addPermission(LocationWhileInUsePermission(), message: message)
continue
case .locationWhenInUse(let message):
pscope.addPermission(LocationWhileInUsePermission(), message: message)
continue
case .contact(let message):
pscope.addPermission(ContactsPermission(), message: message)
continue
case .event(let message):
pscope.addPermission(EventsPermission(), message: message)
continue
case .microphone(let message):
pscope.addPermission(MicrophonePermission(), message: message)
continue
case .camera(let message):
pscope.addPermission(CameraPermission(), message: message)
continue
case .photos(let message):
pscope.addPermission(PhotosPermission(), message: message)
continue
case .reminders(let message):
pscope.addPermission(RemindersPermission(), message: message)
continue
case .bluetooth(let message):
pscope.addPermission(BluetoothPermission(), message: message)
continue
case .motion(let message):
pscope.addPermission(MotionPermission(), message: message)
continue
default:
continue
}
}
 
pscope.show(authChange, cancelled: cancelled)
}
}

然后测试效果(别忘记在Info.plist中添加相关的请求权限的Key-Desc):

1
2
3
4
5
6
7
let permissionTypes = [
INSPermissionType.notification(nil, "打开推送服务"),
INSPermissionType.camera("打开相机服务"),
INSPermissionType.photos("希望使用照片")
]
 
Permission.requestPermission(permissionTypes)

原本计划是要写Push.swift进行截获数据的,但是总感觉这样做貌似不太合理。所以索性我们止只统计一下用户收到推送好了,在Push.swift中仅提供一个方法入口,把推送的内容传进来供我们内部处理。不要干涉AppDelegate处理推送了,而且在iOS10之后要做版本兼容,使用UNUserNotificationCenterDelegate来处理推送,而且大多数第三方SDK都会有自己的处理方式。

所以在我们的Push.swift中,我们先预留一些代码:

等会儿先看个东西:

好、继续写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final public class Push {
public static let `default` = Push()
 
public func DeviceToken(_ deviceToken: Data) {
 
}
 
public func ReceivedPushMessage (_ userInfo: [AnyHashable : Any]) {
 
}
 
private init() {
 
}
}

简单的预留一些方法入口即可,不急着写,接着往下写日志上报(直接改造之前的Logger类):

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
final public class INSLogger {
/// 默认为输出全部日志
public static let `default` = INSLogger()
 
/// 日志级别
public var level: LogLevel = .all
/// 是否上报崩溃
public var crashCollect: Bool = true
 
/// 日志输出
///
/// - parameter lev: 日志级别
/// - parameter content: 日志内容
public func printLog(_ lev: LogLevel, _ details: String, _ items: Any) {
guard level == .all || level == lev, ModeSwitcher.currentMode == .develope else {
return
}
 
print(lev.rawValue, details, "\n", items)
}
private var exception: NSException? = nil
public func setUncaughtException() {
NSSetUncaughtExceptionHandler {
let exception = $0
let name = exception.name
let reason = exception.reason ?? "Without system crash version."
let callStack = exception.callStackSymbols
let crashLog = "name:\(name)\nreason:\(reason)\ncallStack:\(callStack.joined(separator: "\n"))"
 
// TODO: 上报
}
}
}

获取到崩溃的信息后,我们在这里加一个TODO标签。 这里需要注意的是:框架外部如果也需要做日志捕获,那么需要先使用NSGetUncaughtExceptionHandler()获取当前的捕获器,在自己的捕获成功之后也让别人的捕获成功。啊好累啊,这还不是完整的奔溃捕获,于是我们接着写代码(写代码到时无妨,主要是这里有一坑爹的事情,需要自己去查看解决,说明:无法把方法传入这些捕获方法也附上气前一个链接中的代码):

慵懒的完善了signal后(上边提到的不能使用C方法的问题自己去解决把,这里仅仅是展示,所以不写那么详细了):

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
/// 设置异常捕获
public func setUncaughtException() {
 
NSSetUncaughtExceptionHandler {
let exception = $0
let name = exception.name
let reason = exception.reason ?? "Without system crash version."
let callStack = exception.callStackSymbols
let crashLog = "name:\(name)\nreason:\(reason)\ncallStack:\(callStack.joined(separator: "\n"))"
 
exception.raise()
// TODO: 上报
}
 
signal(SIGILL) {
let crashLog = "SignalRaisedException(\($0)): Illegal instruction (not reset when caught)"
// TODO: 上报
}
signal(SIGABRT) {
let crashLog = "SignalRaisedException(\($0)): Abort, abort()"
// TODO: 上报
}
signal(SIGFPE) {
let crashLog = "SignalRaisedException(\($0)): Floating point exception"
// TODO: 上报
}
signal(SIGBUS) {
let crashLog = "SignalRaisedException(\($0)): Bus Error"
// TODO: 上报
}
signal(SIGSEGV) {
let crashLog = "SignalRaisedException(\($0)): segmentation violation"
// TODO: 上报
}
signal(SIGSYS) {
let crashLog = "SignalRaisedException(\($0)): Bad argument to system call"
// TODO: 上报
}
signal(SIGPIPE) {
let crashLog = "SignalRaisedException(\($0)): Write on a pipe with no one to read it"
// TODO: 上报
}
}
 
public func unSetUncaughtException() {
NSSetUncaughtExceptionHandler(nil)
signal(SIGILL, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGSYS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
}

这里只捕获了一部分signal,点进去自己看了解下,我之前也写过一篇关于日志捕获的文章,可以去找找。继续往下写:信息收集,新建swift文件Analytics.swift,这里我只给出一部分思路(完整的Analytics又是一个独立的框架,建议参考的是开源的ZhugeIO):

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
import Foundation
import LKDBHelper
 
let AnalyticsManagerFlushedFlagKey = "AnalyticsManagerFlushedFlagKey"
 
class AnalyticsItem: NSObject {
/// 事件名称
var eventName: String
/// 事件数据
var parameters: [String: Any]?
 
func toAnalytice() ->[String: String] {
return [eventName ?? "": "\(parameters ?? ["": ""])"]
}
 
init(_ eventName: String, _ parameters: [String: Any]? = nil) {
self.eventName = eventName
self.parameters = parameters
}
 
override static func getTableName() -> String {
return "AnalyticsItem"
}
}
 
final public class Analytics {
public static let `default` = Analytics()
public typealias FlushHandler = (_ info: [String], _ analyticsData: [[String: String]])->()
 
/// 设备唯一标识,默认是UUID
public var deviceIdentifier: String
/// 设备用户标识,以设备标识为准
public var userIdentifier: String
/// 上报间隔,会调用上报的方法,外部控制网络请求
public var flushInterval: Int = 10
/// 上报的回调方法
public var flushHandler: FlushHandler?
 
/// 存储准备上报的数组
private var analyticsItems: [AnalyticsItem] = []
/// 是否已经上报,通过检查本地值来确定
private var flushed: Bool
private var timer: Timer? = nil
 
/// 追踪事件
public func track(_ eventName: String, _ parameters: [String: Any]? = nil) {
if analyticsItems.count == 0 {
startTimer()
}
analyticsItems.append(AnalyticsItem(eventName, parameters))
}
 
/// 主动上报到服务器
public func flush() {
guard analyticsItems.count > 0, let handler = flushHandler else {
return
}
handler([deviceIdentifier, userIdentifier], analyticsItems.map { return $0.toAnalytice() })
stopTimer()
flushed = true
}
 
private func startTimer() {
stopTimer()
timer = Timer.init(timeInterval: TimeInterval(flushInterval), target: self, selector: "flush", userInfo: nil, repeats: true)
RunLoop.current.add(timer!, forMode: .commonModes)
}
 
private func stopTimer() {
timer?.invalidate()
timer = nil
analyticsItems.removeAll()
}
 
private func getLocalAnalyticsItem() {
AnalyticsItem.search(withWhere: nil).forEach {
[unowned self] in
self.analyticsItems.append($0 as! AnalyticsItem)
}
let dbHelper = AnalyticsItem.getUsingLKDBHelper()!
dbHelper.dropTable(with: AnalyticsItem.self)
}
 
public func setNeedsRestoreItems() {
analyticsItems.forEach { $0.saveToDB() }
analyticsItems.removeAll()
UserDefaults.standard.set(false, forKey: AnalyticsManagerFlushedFlagKey)
UserDefaults.standard.synchronize()
}
 
public func restoreItems() {
if flushed == false {
getLocalAnalyticsItem()
}
}
 
private func UIApplicationDidEnterBackground() {
setNeedsRestoreItems()
}
 
private func UIApplicationDidBecomeActive() {
self.flushed = UserDefaults.standard.bool(forKey: AnalyticsManagerFlushedFlagKey)
restoreItems()
}
 
private func addListener() {
NotificationCenter.default.addObserver(self, selector: "UIApplicationDidEnterBackground", name: .UIApplicationDidEnterBackground, object: nil)
NotificationCenter.default.addObserver(self, selector: "UIApplicationDidBecomeActive", name: .UIApplicationDidBecomeActive, object: nil)
}
 
private func removeListener() {
stopTimer()
NotificationCenter.default.removeObserver(self)
}
 
private init() {
self.deviceIdentifier = UUID().uuidString
self.userIdentifier = "iOS Device"
self.flushed = UserDefaults.standard.bool(forKey: AnalyticsManagerFlushedFlagKey)
restoreItems()
addListener()
}
 
deinit {
removeListener()
}
}
 
public let AnalyticsManager = Analytics.default

完成之前的奔溃时日志上报

1
2
3
4
// TODO: 上报
AnalyticsManager.track("CRASH", ["info": crashLog])
// 程序奔溃需要调用标记未上传
AnalyticsManager.setNeedsRestoreItems()

写到这里,框架其实只有30%,只有结合业务才能做出与业务相匹配的框架,接下来就是Cache,我只给出代码框架:

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
//存储引擎
public enum IDPStorageType {
case disk
case sql
}
 
//缓存策略
public enum IDPCacheStoragePolicy {
case memory
case disk
case memoryAndDisk
}
 
open class INSCache {
open static let `default` = INSCache()
 
open var _nameSpace: String = "INSCache"
open var _cacheStoragePolicy: IDPCacheStoragePolicy = .memoryAndDisk
open var _memoryCapacity: Float = 0
open var _memoryTotalCost: Float = 0
open var _diskExpiredTime: Int = 0
 
open func existCacheForKey(_ key: String) ->Bool {
return false
}
 
open func clearMemory() {
 
}
 
open func existCacheForKeyInMemory(_ key: String) ->Bool {
return false
}
 
open func existCacheForKeyOnDisk(_ key: String) ->Bool {
return false
}
 
open func setObject(_ data: AnyObject, for key: String) {
 
}
 
open func getObject(for key: String) ->AnyObject? {
return nil
}
 
open func objectForKeyOnlyInMemory(_ key: String) ->AnyObject? {
return nil
}
 
open func asyncObject(forKey key: String, _ handler: (AnyObject)->()) {
 
}
 
open func removeObjcet(for key: String) {
 
}
 
open func removeObjcetForKeyOnlyInMemory(_ key: String) {
 
}
 
open func removeAll () {
 
}
 
open func removeAllInMemory() {
 
}
 
open func removeAllInDisk() {
 
}
 
open class func removeNameSpace(_ spaceName: String) {
 
}
}

包括模型的基类:

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
open class Model: NSObject {
open var _ModelIdentifier: String?
 
open var _ModelUpdatedAt: Date?
open var _ModelCreatedAt: Date?
open var _ModelExpiredAt: Date?
open var _ModelNeedsCache: Bool?
 
open var _CurrentPage: Int = 0
open var _PageSize: Int = 10
open var _TotalCount: Int = 0
open var _StartAt: Int = 0
 
open func load() { }
open func refresh() { }
open func cancel() { }
 
open func goNextPage() { }
open func goPrevPage() { }
 
open func hasPrev() ->Bool{ return false }
open func hasNext() ->Bool{ return false }
 
public override init() {
 
}
}

框架到这里就不说了,接下来有时间就会实际的在使用中一步步的优化框架,让框架适应业务。最近有点忙,开了算法课程,所以框架上边大部分东西都是懒得写,但是使用到的第三方库都建议大家去阅读源码(除ASDK以外)。希望会有所提升。代码地址。仅供作为Swift的语言熟悉,不作为框架教学。

  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    34 引用 • 37 回帖 • 519 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Thymeleaf

    Thymeleaf 是一款用于渲染 XML/XHTML/HTML5 内容的模板引擎。类似 Velocity、 FreeMarker 等,它也可以轻易的与 Spring 等 Web 框架进行集成作为 Web 应用的模板引擎。与其它模板引擎相比,Thymeleaf 最大的特点是能够直接在浏览器中打开并正确显示模板页面,而不需要启动整个 Web 应用。

    11 引用 • 19 回帖 • 333 关注
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 370 关注
  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    34 引用 • 37 回帖 • 519 关注
  • CloudFoundry

    Cloud Foundry 是 VMware 推出的业界第一个开源 PaaS 云平台,它支持多种框架、语言、运行时环境、云平台及应用服务,使开发人员能够在几秒钟内进行应用程序的部署和扩展,无需担心任何基础架构的问题。

    5 引用 • 18 回帖 • 148 关注
  • 深度学习

    深度学习(Deep Learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。

    46 引用 • 40 回帖
  • 创业

    你比 99% 的人都优秀么?

    83 引用 • 1398 回帖 • 1 关注
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 614 关注
  • BAE

    百度应用引擎(Baidu App Engine)提供了 PHP、Java、Python 的执行环境,以及云存储、消息服务、云数据库等全面的云服务。它可以让开发者实现自动地部署和管理应用,并且提供动态扩容和负载均衡的运行环境,让开发者不用考虑高成本的运维工作,只需专注于业务逻辑,大大降低了开发者学习和迁移的成本。

    19 引用 • 75 回帖 • 620 关注
  • 宕机

    宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。

    13 引用 • 82 回帖 • 51 关注
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖 • 10 关注
  • Sublime

    Sublime Text 是一款可以用来写代码、写文章的文本编辑器。支持代码高亮、自动完成,还支持通过插件进行扩展。

    10 引用 • 5 回帖 • 2 关注
  • FlowUs

    FlowUs.息流 个人及团队的新一代生产力工具。

    让复杂的信息管理更轻松、自由、充满创意。

    1 引用 • 1 关注
  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    717 引用 • 1241 回帖 • 79 关注
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    20642 引用 • 80648 回帖 • 1 关注
  • Quicker

    Quicker 您的指尖工具箱!操作更少,收获更多!

    28 引用 • 101 回帖
  • Kubernetes

    Kubernetes 是 Google 开源的一个容器编排引擎,它支持自动化部署、大规模可伸缩、应用容器化管理。

    109 引用 • 54 回帖
  • Tomcat

    Tomcat 最早是由 Sun Microsystems 开发的一个 Servlet 容器,在 1999 年被捐献给 ASF(Apache Software Foundation),隶属于 Jakarta 项目,现在已经独立为一个顶级项目。Tomcat 主要实现了 JavaEE 中的 Servlet、JSP 规范,同时也提供 HTTP 服务,是市场上非常流行的 Java Web 容器。

    162 引用 • 529 回帖 • 4 关注
  • Bootstrap

    Bootstrap 是 Twitter 推出的一个用于前端开发的开源工具包。它由 Twitter 的设计师 Mark Otto 和 Jacob Thornton 合作开发,是一个 CSS / HTML 框架。

    18 引用 • 33 回帖 • 670 关注
  • SVN

    SVN 是 Subversion 的简称,是一个开放源代码的版本控制系统,相较于 RCS、CVS,它采用了分支管理系统,它的设计目标就是取代 CVS。

    29 引用 • 98 回帖 • 698 关注
  • WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,主要功能包括:位置相关的内容递送;用户认证和授权;集中式网络监控。

    1 引用 • 7 回帖 • 576 关注
  • API

    应用程序编程接口(Application Programming Interface)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

    76 引用 • 429 回帖 • 4 关注
  • SQLite

    SQLite 是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是全世界使用最为广泛的数据库引擎。

    4 引用 • 7 回帖 • 2 关注
  • SpaceVim

    SpaceVim 是一个社区驱动的模块化 vim/neovim 配置集合,以模块的方式组织管理插件以
    及相关配置,为不同的语言开发量身定制了相关的开发模块,该模块提供代码自动补全,
    语法检查、格式化、调试、REPL 等特性。用户仅需载入相关语言的模块即可得到一个开箱
    即用的 Vim-IDE。

    3 引用 • 31 回帖 • 83 关注
  • Solidity

    Solidity 是一种智能合约高级语言,运行在 [以太坊] 虚拟机(EVM)之上。它的语法接近于 JavaScript,是一种面向对象的语言。

    3 引用 • 18 回帖 • 364 关注
  • Love2D

    Love2D 是一个开源的, 跨平台的 2D 游戏引擎。使用纯 Lua 脚本来进行游戏开发。目前支持的平台有 Windows, Mac OS X, Linux, Android 和 iOS。

    14 引用 • 53 回帖 • 526 关注
  • WordPress

    WordPress 是一个使用 PHP 语言开发的博客平台,用户可以在支持 PHP 和 MySQL 数据库的服务器上架设自己的博客。也可以把 WordPress 当作一个内容管理系统(CMS)来使用。WordPress 是一个免费的开源项目,在 GNU 通用公共许可证(GPLv2)下授权发布。

    58 引用 • 113 回帖 • 274 关注
  • 京东

    京东是中国最大的自营式电商企业,2015 年第一季度在中国自营式 B2C 电商市场的占有率为 56.3%。2014 年 5 月,京东在美国纳斯达克证券交易所正式挂牌上市(股票代码:JD),是中国第一个成功赴美上市的大型综合型电商平台,与腾讯、百度等中国互联网巨头共同跻身全球前十大互联网公司排行榜。

    14 引用 • 102 回帖 • 395 关注