《Clojure Web开发实战》——第1章,第1.2节你的第一个工程

1.2 你的第一个工程
你的留言簿应该已经在控制台运行了,可以通过http://localhost:3000/来访问。在控制台终端按下Ctrl+C,就能停止它的运行。既然我们已经在Light Table的工作区打开了这个工程,不妨就直接在编辑器中运行它吧。
我们现在要更进一步,创建一个 “读取—求值—打印循环”(REPL,Read-Evaluate-Print Loop),将Light Table连接至我们的工程。菜单View →Connections可以打开连接标签页。如图1-4所示,让我们点击标签页中的Add Connection按钮。
图1-4 Light Table的连接

此时,会弹出一个列表,列出了几种不同的连接选项。如图1-5所示,接下来选择Clojure。然后,让我们找到留言簿工程所在的文件夹,并且选中project.clj文件。
图1-5 Light Table连接Clojure

一旦我们的工程与Light Table建立了连接,我们就可以直接在编辑器中对代码进行求值了。
说不如做,你可以立刻挑选一个函数,然后按下Ctrl+Enter(Windows/Linux)组合键或是Cmd+Enter(OS X)组合键。如果我们选择的是home函数,那么打印出来的内容应该是这样:
'guestbook.routes.home/home
这意味着这个函数已经在REPL中进行了求值,随时可用了。
另外,按下Ctrl+spacebar组合键后输入repl,就能打开一个即时repl。在这个新打开的编辑器窗格中,我们可以随意运行任何代码,如图1-6所示。

图1-6 Light Table的即时repl

默认情况下,一旦进行任何修改,都会使得即时repl中的所有内容被重新求值。这被称为live实时模式。现在,让我们载入guestbook.repl命名空间,然后执行start-server函数。
(use 'guestbook.repl)
(start-server)

一旦上述代码完成求值,就会启动HTTP服务,同时打开一个新的浏览器窗口,指向了应用的主页,如图1-7所示。

图1-7 在即时repl中运行服务

显然我们不希望start-server被反复调用,因此记得从即时repl里删除之前的代码。
另外,我们还可以关闭实时求值功能,只要点击右上角的live图标即可。禁用了实时模式后,我们可以通过Alt-Enter来进行选择性的求值。
下面,如图1-8所示,让我们执行(use 'guestbook.routes.home)来导入home命名空间,然后调用home函数。
如你所见,对home的调用只是简单生成了我们的HTML主页,一个字符串。这就是我们访问http://localhost:3000时,浏览器为我们呈现出来的内容。

图1-8 使用REPL

值得注意的是,在我们的代码中使用了Clojure的vector(矢量表)来表达相应的HTML标签。如果我们添加一些新的标签,并在浏览器中刷新页面的话,立刻就能看到变化。例如,让我们对home函数稍事修改,让它能够显示标题,并提供一个用于录入消息的表单。
(defn home []
 (layout/common
 [:h1 "Guestbook"]
 [:p "Welcome to my guestbook"]
 [:hr]
 [:form
  [:p "Name:"]
  [:input]
  [:p "Message:"]
  [:textarea {:rows 10 :cols 40}]]))

好了,刷新一下页面,看到变化了吧,如图1-9所示。

图1-9 留言簿

你可能已经猜到了,紧接着home函数的那几行代码,就是负责将“/”路由和处理函数home绑到一块儿的。
(defroutes home-routes
 (GET "/" [] (home)))

此处,我们使用defroutes来定义guestbook.routes.home命名空间中的路由。每个路由都代表着一个应用会响应的URI地址。路由定义的起始位置是HTTP请求的类型,例如,GET或者POST,接下来则是参数和主体部分。
我们还会为这个工程添加更多的功能,在此之前,让我们了解一下Leiningen模板为我们生成了哪些文件吧。
了解应用程序的结构
在Workspace标签页中展开我们的工程之后,看上去应该是这样的:
guestbook/
 resources/
  public/
   css/
    screen.css
   img/
   js/
 src
  guestbook/
  models/
  routes/
   home.clj
  views/
   layout.clj
  handler.clj
  repl.clj
test/
 guestbook/
 test/
   hanlder.clj
project.clj
README.md

位于工程根目录下的project.clj文件是用于配置和构建应用的。
还有几个文件夹,src用来存放应用的代码。resources文件夹则用来存放与应用相关的静态资源,比如CSS、图片和JavaScript脚本。最后,在test文件夹中,我们可以为应用添加一些测试。
Clojure命名空间遵循Java的打包约定,也就是说,如果命名空间包含前缀,则其存放的文件夹路径必须与前缀相匹配。需要注意的是,如果一个命名空间包含“-”,则体现在文件夹路径和文件名上时,“-”必须转换为“_”。
这是因为Java的包名中不允许出现“-”。而Clojure代码最终会被编译为JVM字节码,所以也必须遵守这个规则。
由于我们把自己的应用叫作guestbook,因此它所有的命名空间都被放置在了src/guestbook文件夹下。让我们看看都有些什么吧。首先,我们在src/guestbook/handler.clj文件中找到了guestbook.handler命名空间。这个命名空间包含了应用程序的入口点,此外还定义了被用来处理所有请求的handler。
在src/guestbook/repl.clj文件中的是guestbook.repl命名空间,调用里面的函数,就可以在REPL中启动和停止服务。我们可以借助它直接从编辑器中启动我们的应用,而不必非得通过lein来运行。
接下来,我们有一个名为models的文件夹。这是留给应用的模型层的。里面的命名空间也负责连接数据库、定义表结构,还有访问记录等。
在routes文件夹下,是那些负责定义路由的命名空间。这些路由构成了我们将要实现的工作流的入口点。
目前,我们只有一个被称为guestbook.routes.home的命名空间,应用的主页就是在这里定义的。这个命名空间位于src/guestbook/routes/home.clj文件中。
接下来的文件夹是views,里面的命名空间通常负责应用的界面布局。其自带的命名空间guestbook.views.layout定义了页面的基本结构。显而易见,这个命名空间对应的文件就是src/guestbook/views/layout.clj。
添加一些功能
让我们来看看如何为留言簿应用创建用户界面(UI,user interface)吧。即使你阅读这些代码会感觉有点吃力,也不必担心,在后面的章节中你还有机会弄明白。相比纠缠于每个函数的细枝末节,目前应把注意力放在如何组织我们的应用,以及如何拆分应用逻辑更为重要。
在前面,我们曾经用纯手工的方式创建了一个录入表单。现在,我们打算用一个更好的实现来替代它,这会用到Hiccup10库提供的辅助函数。
为了使用这些函数,需要把库导入我们的命名空间,像下面这样修改命名空间的声明:
(ns guestbook.routes.home
  (:require [compojure.core :refer :all]
       [guestbook.views.layout :as layout]
       [hiccup.form :refer :all]))

首先我们创建一个函数,用来呈现已有的消息。这个函数会生成一个包含了现有消息的HTML列表。就目前来说,我们先简单地硬编码几条消息就行。
(defn show-guests []
[:ul.guests
  (for [{:keys [message name timestamp]}
     [{:message "Howdy" :name "Bob" :timestamp nil}
      {:message "Hello" :name "Bob" :timestamp nil}]]
  [:li
    [:blockquote message]
    [:p "-" [:cite name]]
    [:time timestamp]])])

接下来,我们对home函数进行调整,使顾客可以看到前面那些顾客留下的消息。当然,还得提供一个表单用来创建新的消息。
(defn home [& [name message error]]
(layout/common
  [:h1 "Guestbook"]
  [:p "Welcome to my guestbook"]
  [:p error]
  ;here we call our show-guests function
  ;to generate the list of existing comments
  (show-guests)
  [:hr]
  ;here we create a form with text fields called "name" and "message"
  ;these will be sent when the form posts to the server as keywords of
  ;the same name
  (form-to [:post "/"]
   [:p "Name:"]
   (text-field "name" name)
   [:p "Message:"]
   (text-area {:rows 10 :cols 40} "message" message)
   [:br]
   (submit-button "comment"))))

切换到浏览器,可以看到两条测试消息连同表单一块儿都显示出来了。请留意,现在home函数多了几个可选参数。我们会把这些参数的值显示到页面上。如果这些参数为nil,那么在进行显示时,会把它们视作空字符串。
我们创建的这个表单会向“/”发送HTTP的POST请求,所以我们再添加一个路由来处理它吧:这个路由将会调用一个名为save-message的辅助函数,我们稍后会给出其定义。
guestbook/src/guestbook/routes/home.clj
(defroutes home-routes
(GET "/" [] (home))
(POST "/" [name message] (save-message name message)))

save-message函数会检查name和message这两个参数,然后就去调用home函数。倘若两个参数都没问题,那么消息会被打印到控制台;否则,将会生成一条出错信息。
(defn save-message [name message]
(cond
  (empty? name)
  (home name message "Some dummy forgot to leave a name")
  (empty? message)
  (home name message "Don't you have something to say?")
  :else
  (do
   (println name message)
   (home))))

来,在留言簿中留一次言试试看,你会看到名字和消息在控制台里打印出来了。接下来,将name或者message留白,看看有没有显示出错消息。
现在,视图部分已经具备了通过UI显示和提交消息的能力。但此时此刻,我们还没有能真正存放这些消息的地方。
添加数据模型
既然我们的应用需要保存访客们的留言,那我们在project.clj11文件中加入对JDBC和SQLite的依赖项吧。添加完毕后的,:dependencies看起来应该是下面这样子的:
:dependencies [[org.clojure/clojure "1.5.1"]
         [compojure "1.1.5"]
         [hiccup "1.0.4"]
         [ring-server "0.3.0"]
         ;;JDBC dependencies
         [org.clojure/java.jdbc "0.2.3"]
         [org.xerial/sqlite-jdbc "3.7.2"]]

因为添加了新的依赖项,我们需要将工程与REPL重新连接。首先打开Connect标签页并且点击disconnect按钮,然后按照先前介绍过的步骤来连接一个新的REPL实例,如图1-10所示。

图1-10 断开REPL

一旦重新连上了REPL,我们就需要在即时repl中执行(start-server),早些时候我们曾经做过一次,还记得吗?
OK,万事俱备,只欠数据模型了。我们会在src/guestbook/models文件夹下创建一个新的命名空间。我们把这个命名空间称为guestbook.models.db。具体做法是:在工作区中,右键单击models文件夹,并且选择New File选项,然后将这个文件命名为db.clj。
正如其名称所暗示的,db命名空间将负责应用的数据模型,并且提供从数据库读取或是向数据库写入数据的功能。
首先,我们需要添加命名空间声明,以及导入数据库依赖项。下面是这个命名空间的声明:
guestbook/src/guestbook/models/db.clj
(ns guestbook.models.db
 (:require [clojure.java.jdbc :as sql])
 (:import java.sql.DriverManager))

请注意,导入其他Clojure命名空间时,我们使用的是:require关键字,而导入Java类时,我们则用了:import。
下一步,我们将要创建数据库连接的定义。这个定义其实就是一个简单的map,包含了JDBC驱动的类型、协议,以及SQLite数据库的文件名。
guestbook/src/guestbook/models/db.clj
(def db {:classname "org.sqlite.JDBC",
     :subprotocol  "sqlite",
     :subname     "db.sq3"})

声明了数据库连接之后,我们还需要编写一个函数,创建用于保存访客留言的数据表。
guestbook/src/guestbook/models/db.clj
(defn create-guestbook-table []
 (sql/with-connection
  db
  (sql/create-table
   :guestbook
   [:id "INTEGER PRIMARY KEY AUTOINCREMENT"]
   [:timestamp "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"]
   [:name "TEXT"]
   [:message "TEXT"])
  (sql/do-commands "CREATE INDEX timestamp_index ON guestbook (timestamp)")))

这个函数使用了with-connection语句,这样就能确保数据库连接在使用完毕后能够得到恰当的清理。在其内部,我们调用create-table函数来创建数据表,表名用关键字表示,而表示字段则使用了vector。为了完整起见,我们还为timestamp字段创建了索引。
在即时repl中执行(create-guestbook-table)之前,我们首先要导入它的命名空间,前面我们曾经对guestbook.routes.home也这么做过,还记得吗?
(use 'guestbook.models.db)
(create-guestbook-table)

现在你就可以在即时repl中执行create-guestbook-table,把数据表给创建出来了。但有一点需要注意,如果你开启了实时模式,那么最好将其禁用;否则每次即时repl临时缓冲的变动,都会导致create-guestbook-table被调用并产生错误。
数据表创建完毕,接下来我们就可以编写从数据库中读取留言的函数了。
guestbook/src/guestbook/models/db.clj
(defn read-guests []
 (sql/with-connection
  db
  (sql/with-query-results res
    ["SELECT * FROM guestbook ORDER BY timestamp DESC"]
    (doall res))))

此处我们使用with-query-results与来执行select语句,并返回其结果。之所以要在返回之前调用doall,是因为res是惰性的,不会把所有结果都加载到内存中。
通过调用doall,我们强制对res进行了完全求值。如果不这么做的话,一旦离开了函数的作用范围,我们的数据库连接就会被关闭,于是便无法在函数之外访问结果数据了。
我们还需要创建另外一个函数,用来把消息保存到留言簿的数据表中。这个函数会调用insert-values,并且接受访客的名字和消息作为参数进行保存。
guestbook/src/guestbook/models/db.clj
(defn save-message [name message]
 (sql/with-connection
  db
  (sql/insert-values
    :guestbook
    [:name :message :timestamp]
    [name message (new java.util.Date)])))

用于读取和保存消息的函数已经写好,现在我们可以在REPL中尝试一下了。我们需要在即时repl中重新执行一遍(use 'guestbook.models.db),这样才能访问这几个新添加的函数。然而,在guestbook.models.db和guestbook.routes.home这两个命名空间中都定义了名为save-message的函数。
如果尝试重新加载guestbook.models.db命名空间,我们会得到一个错误,指出save-message已经从guestbook.routes.home命名空间导入过了。为了避免这个问题,在执行(use 'guestbook.models.db)之前,我们需要在即时repl中先执行ns-unmap,移除当前对save-message的引用。
(ns-unmap 'user 'save-message)
(use 'guestbook.models.db)

现在我们可以尝试运行下面的代码,看看保存和读取消息的逻辑是否符合预期:
(save-message "Bob" "hello")
(read-guests)

将留言保存到数据库,然后读取出来以后,我们应该能看到图1-11所示的输出。
有了持久层,我们就可以回过头去修改home命名空间,将先前那些硬编码的假数据统统扔掉了。
组合起来
现在我们可以把对db的依赖项添加到home路由的命名空间声明中了。
guestbook/src/guestbook/routes/home.clj
(ns guestbook.routes.home
 (:require [compojure.core :refer :all]
      [guestbook.views.layout :as layout]
      [hiccup.form :refer :all]
      [guestbook.models.db :as db]))

接下来,我们需要修改show-guests函数,让它去调用db/read-guests:

图1-11 测试保存功能

(defn show-guests []
[:ul.guests
  (for [{:keys [message name timestamp]} (db/read-guests)]
  [:li
    [:blockquote message]
    [:p "-" [:cite name]]
    [:time timestamp]])])

最后,我们还得修改save-message函数,让它调用db/save-message,而不是简单地把参数打印出来:
guestbook/src/guestbook/routes/home.clj
(defn save-message [name message]
(cond
  (empty? name)
  (home name message "Some dummy forgot to leave a name")
  (empty? message)
  (home name message "Don't you have something to say?")
  :else
  (do
   (db/save-message name message)
    (home))))

完成这些修改之后,我们都迫不及待地要打开浏览器中看看效果如何啦。不出所料,先前我们在REPL中添加到数据库中的那条消息显示出来了,如图1-12所示。

图1-12 真正的留言

我们还可以试着多录入几条消息,以确认留言簿的功能确实符合预期。
你也许注意到了,页面上消息的显示是存在缺陷的。时间只是简单的显示为毫秒数。这实在是太不友好了,所以,让我们添加一个改善其格式的函数吧。
为此,我们将会创建一个Java的SimpleDateFormat对象,用来对时间戳进行格式化。
guestbook/src/guestbook/routes/home.clj
(defn format-time [timestamp]
 (-> "dd/MM/yyyy"
    (java.text.SimpleDateFormat.)
    (.format timestamp)))
(defn show-guests []
 [:ul.guests
  (for [{:keys [message name timestamp]} (db/read-guests)]
   [:li
     [:blockquote message]
     [:p "-" [:cite name]]
     [:time (format-time timestamp)]])])

收尾
我们的留言簿应用已接近完成,还剩下最后一个问题。
由于我们需要先创建数据库,其后才能访问,所以还需要往handler命名空间中添加一些代码。首先,我们需要在handler中导入命名空间db。
(ns guestbook.handler
...
(:require ...
       [guestbook.models.db :as db]))
接下来修改init函数,检查数据库是否存在,如果不存在则创建之。
guestbook/src/guestbook/handler.clj
(defn init []
 (println "guestbook is starting")
 (if-not (.exists (java.io.File. "./db.sq3"))
  (db/create-guestbook-table)))

由于应用加载时会调用init函数,因此就能确保数据库在真正开始运行之前便已经准备妥当了。
你学到了什么
通过前面这个例子,我们体验了如何使用Clojure来开发Web应用。你也许已经注意到了,你只是编写了极少的代码,就得到了一个可用的程序。而且,你几乎没有编写任何样板代码。
阅读至此,你对程序结构、主要组件,以及如何将它们组合到一起应该相当熟悉了。
回顾一下,我们的应用包含了以下几个命名空间。
命名空间guestbook.handler的职责是启动服务,并创建一个handler,负责把来自客户端的请求传递给应用。
然后是命名空间guestbook.routes.home。我们在这里建立了留言功能的工作流程,同时大部分应用逻辑也都位于此处。如果需要添加更多的工作流,你需要在guestbook.routes下创建新的命名空间。例如,你可能会创建guestbook.routes.auth命名空间,用来处理用户注册和认证。
通常,routes文件夹下的每个命名空间都封装着应用中一个自包含的工作流程。所有与之相关的代码都位于同一个地方,并且与其他的路由保持独立。此处工作流表示的可能是用户认证,也可能是内容编辑,或是事务管理等。
命名空间guestbook.views.layout负责管理应用的界面布局。我们会在这里放置一些代码,用来生成页面的公共元素,以及控制页面的结构。一般来说,布局方面需要考虑的内容包括:组织静态资源,比如页面需要的CSS和JavaScript文件;设置公共元素,比如页眉和页脚等。
最后,还有命名空间guestbook.models.db,它负责整个应用的数据模型。联系例子中定义的数据表,它描述了数据的类型,以及哪些用户的数据需要持久化。
当我们着手构建更大规模的应用时,这些东西应该牢记于胸。一个结构良好的Clojure应用会易于理解,也方便维护。对于有些编程语言,当代码规模较大时,你得费尽心思才能理清其复杂的层次结构。而在Clojure应用的整个生命周期中,你都不会有类似的烦恼,这真是太美妙了。
我们使用了Light Table来开发留言簿应用。虽然它相当易用,但仍需更多打磨,还缺乏一些其他集成开发环境(IDE,Integrated Development Environments)提供的有用特性。这些特性包括代码完成、结构化的代码编辑,以及集成的依赖管理。
所以,我建议你花些时间去尝试一下那些更为成熟的开发环境,例如Eclipse12或者Emacs13。本书的剩余部分假定以Eclipse作为我们的开发环境,不过,无论你选用的是哪种编辑器,都没有任何问题。如需了解其他可选的IDE,不妨参考“附录1 选择IDE”。
你会发现,在开发应用的过程中,我们大量使用了REPL。因此,对于Clojure开发环境而言,是否集成了REPL可谓有着天壤之别。能在REPL中执行代码,就意味着你能获得更快的反馈周期,从而显著地提升生产力。
在本章中,我们演示了如何设置开发环境,以及如何搭建一个典型的Clojure Web应用。下一章,我们将关注那些构成Clojure Web栈的核心库。你将会了解到以下内容:请求响应的生命周期、定义路由、会话管理,以及利用中间件强化核心处理请求功能。

时间: 2024-10-06 22:55:28

《Clojure Web开发实战》——第1章,第1.2节你的第一个工程的相关文章

《Clojure Web开发实战》——导读

目 录第1章 起步 第1章第1节环境设置第1章第2节你的第一个工程第2章 Clojure的Web技术栈 第2章第1节使用Ring来路由请求第2章第2节定义Compojure路由第2章第3节应用架构第2章第4节Compojure和Ring之后第2章第5节你学到什么第3章 服务组件Liberator第4章 访问数据库第5章 相册第6章 收尾第7章 混合附录1 选择IDE附录2 Clojure入门附录3 面向文档的数据库访问

《Clojure Web开发实战》——第2章,第2.3节应用架构

2.3 应用架构典型的Compojure开发Web程序方式可能不同于你之前使用的方式.多数框架偏好使用模型-视图-控制器(MVC,model-view-controller)模式使用逻辑分离思想将视图.控制.模式严格分开.这里,Compojure并没有明确分离视图和控制.相反,我们为程序中每个路由创建了独立的handler,这些handler用于处理来自客户端的HTTP请求,Compojure正是以这种思路来分派任务的.handler驱动模型负责处理域逻辑.这种方法提供了一个彻底的域逻辑分离模式

《Clojure Web开发实战》——第2章,第2.4节Compojure和Ring之后

2.4 Compojure和Ring之后不少程序库能有效应对各种处理任务,比如会话管理.输入验证.身份认证.你依旧可以随意挑拣适合你的部件.我们选择lib-noir19作为接下来的关注重点,因为应对Web程序的绝大多数任务,它都能胜任.我们之前通过介绍Hiccup的API,学习了它的一些特性及常见功能,同样,我们也来看看lib-noir是如何用的.首先,为了能启用lib-noir,我们需将其添入项目描述文件project.clj.具体是在依赖项的vector里添加[lib-noir "0.7.6

《Clojure Web开发实战》——第2章,第2.2节定义Compojure路由

2.2 定义Compojure路由Compojure是构建在Ring之上的路由库,它提供的方式非常简洁,用来关联处理URL和HTTP方法.Compojure路由基本上是这样子的:(GET "/:id" [id] (str "<p>the id is: " id "</p>" ))其路由函数名与HTTP方法名直接对应,比如GET.POST.PUT.DELETE和HEAD.还有一个称为ANY的路由会响应客户端任何方法.URI是

《Clojure Web开发实战》——第2章,第2.5节你学到什么

2.5 你学到什么在这一章,我们见识了如何通过Clojure搭建Web栈,以及一些常用程序库.我们谈及了如何与Ring.Compojure.lib-noir交互,通过完成比如输入验证和会话管理的任务来说明它们之间如何相互作用.但愿你已能顺畅阅读,并理解在留言簿项目(我们在"第1章 起步"创建的那个项目)的代码.如果你还有疑惑,我强烈建议你去重新阅读"第1章",并在REPL环境中尝试自己搭建这个例子.如果你还没来得及做,再提一点,借此机会把本章的例子带入留言簿程序做一

《HTML5移动Web开发实战》—— 第1章 HTML5与移动网站

第1章 HTML5与移动网站 HTML5移动Web开发实战 本章内容包括: 准备好你的移动设备 仿真器与模拟器 搭建移动开发环境 在移动网站中使用HTML5 跨浏览器兼容HTML5 适用于移动设备的设计 确定你的核心移动设备 定义一个内容策略

《高性能响应式Web开发实战》一第2章 响应式中要面对的问题

第2章 响应式中要面对的问题 高性能响应式Web开发实战响应式设计的主要工作就是要让网页适配当下种类繁多的设备,使页面在不同设备上仍然看上去友好并且可用.但是细想,当在设法让一个页面同时适配三星Galaxy S6和iPhone 6时,我们究竟是在适配什么?Galaxy S6和iPhone 6究竟存在哪些影响页面展现的差异因素?以上这些问题都可以归纳为:当谈论设备的时候我们究竟在谈论什么? 不同设备间的差异有很多种,我们不关心设备的制造厂商,不关心CPU功耗,不关心生产工艺,只关心会影响页面在屏幕

《高性能响应式Web开发实战》一第1章 概述及任务介绍

第1章 概述及任务介绍 高性能响应式Web开发实战本章向读者大致描述整本书的轮廓.希望通过阅读本章内容,读者能够了解这本书涉及的技术范围.写作风格.写作思路以及贯穿全文的线索.我相信这对读者阅读接下来的内容会很有帮助,不至于让读者觉得某些章节的安排比较突兀. 当然,读者也可以跳过本章内容,直接进入下一章,开始实战技术的学习.

《高性能响应式Web开发实战》一导读

前 言 高性能响应式Web开发实战 为什么写这样一本书 作为一名程序员,写书也好,写博客也罢,其实都和写开源程序的性质是一样的,都是想要把自己的知识分享出去.分享是一件非常有成就感同时也是很快乐的事情,因为我们在此过程中会有很多新的想法,会迫不及待地想去实现,也会有很多人来和我们进行交流,探讨其他的一些可能性.最重要的是,对于做分享的人而言,做好分享很难!首先,分享者要对自己讲解的技术有足够的了解,不仅仅是了解如何用它,还要了解它的过去和未来:其次,分享者要能够娓娓道来,要站在受众的立场上考虑他