
“Talk is cheap, show me your code!”
当一个程序员在做技术分享的时候, 代码演示经常是不可或缺的一个环节。然而在你的演示PPT和代码运行之间切换是一件非常恼人事情,而且非常影响演示的节奏和流畅性。要做一个完美的技术分享,能不能把代码的运行嵌入到PPT中呢?
当然可以,我前不久(几年前把 )在公司内部分享了一起关于Python的小谜题,大家可以到斗鱼去观看这次分享的视频录播 (很糟糕的是在36分钟后,摄像头偏移了,拍了大半天挂钟)
为了实现代码嵌入ppt,我用到的关键技术包括:
- Microsoft PowerPoint
- Glot Code Runner
- Docker
首先,演示常用的是微软的PowerPoint(至于苹果的Keynote,我暂时还没有找到解决方案),所以你需要安装PowerPoint,在最新的Office套件中,微软提供了一个Webview的插件,有了这个插件,可以直接把一个Web页面,嵌入到Office文档中,自然也就包含了PPT。

注意,为了安全性的考虑,嵌入的web页面必须是https的。
好了有了这个,我们就可以把一个代码运行的web页面直接嵌入到演示文档中,剩下的事情,就是做一个可以运行不同代码的Web应用了。
Glot 是一个开源的在线代码运行平台。其架构如下图所示。

利用Glot的代码运行的核心功能,我们就可以很方便的开发一个可以运行各种代码的web应用。
利用容器,我们可以把整个应用分为以下的几个层次:
- Base
提供基本的代码运行的环境,包含了代码执行的必要的解释器和编译器。在本次演示中,我使用了golang:latest,但是碰巧的是这个镜像是拥有Python的解释器的,我的代码演示都是python,省去了我安装Python的步骤。如果是别的不同的语言的演示,注意安装对应的环境。 - Code Runner https://github.com/gangtao/code_runner
我的Code Runner是对Glot的Code Runner的一个增强,该项目提供一个运行代码的服务。
项目的Dockerfile:
FROM golang:latest
MAINTAINER gangtao@outlook.com
ENV GOPATH=/home/glot
ENV GOROOT=/usr/local/go
# Add user
RUN groupadd glot
RUN useradd -m -d /home/glot -g glot -s /bin/bash glot
# Copy files
Add ./build/release/server /home/glot/
# Add ./vendor/. /home/glot/src
USER glot
WORKDIR /home/glot/
# generate certificate
RUN go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost
EXPOSE 8080
# CMD ["/home/glot/runner"]
ENTRYPOINT ["/home/glot/server"]
通过Dockerfile我们可以看出该项目主要的内容:利用Glot实现代码运行,产生证书用于https,利用echo实现web 服务。
Web服务的代码如下:
package code_runner
import (
"fmt"
"github.com/prasmussen/glot-code-runner/cmd"
"github.com/prasmussen/glot-code-runner/language"
"io/ioutil"
"os"
"path/filepath"
)
type Payload struct {
Language string `json:"language"`
Files []*InMemoryFile `json:"files"`
Stdin string `json:"stdin"`
Command string `json:"command"`
}
type InMemoryFile struct {
Name string `json:"name"`
Content string `json:"content"`
}
type Result struct {
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
Error string `json:"error"`
}
func Run(payload *Payload) *Result {
// Ensure that we have at least one file
if len(payload.Files) == 0 {
exitF("No files givenn")
}
// Check if we support given language
if !language.IsSupported(payload.Language) {
exitF("Language '%s' is not supportedn", payload.Language)
}
// Write files to disk
filepaths, err := writeFiles(payload.Files)
if err != nil {
exitF("Failed to write file to disk (%s)", err.Error())
}
var stdout, stderr string
// Execute the given command or run the code with
// the language runner if no command is given
if payload.Command == "" {
stdout, stderr, err = language.Run(payload.Language, filepaths, payload.Stdin)
} else {
workDir := filepath.Dir(filepaths[0])
stdout, stderr, err = cmd.RunBashStdin(workDir, payload.Command, payload.Stdin)
}
result := &Result{
Stdout: stdout,
Stderr: stderr,
Error: errToStr(err),
}
return result
}
// Writes files to disk, returns list of absolute filepaths
func writeFiles(files []*InMemoryFile) ([]string, error) {
// Create temp dir
tmpPath, err := ioutil.TempDir("", "")
if err != nil {
return nil, err
}
paths := make([]string, len(files), len(files))
for i, file := range files {
path, err := writeFile(tmpPath, file)
if err != nil {
return nil, err
}
paths[i] = path
}
return paths, nil
}
// Writes a single file to disk
func writeFile(basePath string, file *InMemoryFile) (string, error) {
// Get absolute path to file inside basePath
absPath := filepath.Join(basePath, file.Name)
// Create all parent dirs
err := os.MkdirAll(filepath.Dir(absPath), 0775)
if err != nil {
return "", err
}
// Write file to disk
err = ioutil.WriteFile(absPath, []byte(file.Content), 0664)
if err != nil {
return "", err
}
// Return absolute path to file
return absPath, nil
}
func exitF(format string, a ...interface{}) {
fmt.Fprintf(os.Stderr, format, a...)
os.Exit(1)
}
func errToStr(err error) string {
if err != nil {
return err.Error()
}
return ""
}
- 可以通过容器方便的启动该服务,然后就可以通过Rest请求,执行Python(Golang)的代码。
curl
-X POST
http://localhost:8080/run
-H 'Content-Type: application/json'
-d '{"language":"python","files":[{"name":"main.py","content":"print(42)"}]}'
{"stdout":"42n","stderr":"","error":""}
- 在这个项目中,我用了echo来实现一个轻量级的Web服务,而没有使用Glot自带的基于Ruby的服务,这样做的好处是技术栈的统一,因为echo和glot的核心都是用的Golang。
- Code Runner Web https://github.com/gangtao/code_runner_web
有了服务,下面就是前端的UI了。
UI使用了codemirror来做编辑器。Dockerfile如下:
FROM naughtytao/code_runner
MAINTAINER gangtao@outlook.com
Add ./static /home/glot/static
- 运行Code Runner Web后,就可以在以下的web界面中输入你想要运行的结果,并实时的显示想要运行的结果了。

好了,剩下的还有一些事情要做,就是准备你的演示代码,大家可以参考这个项目,这里缺省是将所有的代码片段放在code/python/ 目录下。运行:http://localhost:8080/#2 就会加载第二个代码片段。
这个是嵌入后的效果:

好了,是不是很Coooooool呢?
另外利用容器来构建应用真的非常非常方便。