最近的一个项目,需要实现一个工作任务流(task pipeline),基于之前CICD的经验,jenkins pipeline和drone的pipeline进入候选。

drone是基于go的cicd解决方案,github上有1.6万+star,本文简单对比了其和jenkins的区别,重点介绍了drone的pipeline原理,并简单分析了代码。

jenkins 与 drone

对比项 jenkins drone
pipeline定义 编写jenkinsfile 编写流程yml
运行方式 在一个pod里运行 每一步骤起对应的container,通过挂载volume实现数据共享
运行环境 物理机或者容器环境,包括K8S docker容器环境
开发语言 java golang

drone pipeline好处是相对更轻量级,yml定义也相对简洁清晰,按照功能来划分容器,可以方便的实现task的复用,而jenkins则是完全打包到一个镜像,会造成单个镜像体积过大,比如jenkins的单个镜像超过2G。

drone的pipeline,是基于https://github.com/cncd/pipeline 实现的,这里简单分析下其原理。

编译和执行 drone pipeline

要了解一个程序的原理,先从输入输出讲起。

先安装:

  1. go get -u github.com/cncd/pipeline
  2. go install github.com/cncd/pipeline/pipec

然后测试

  1. cd $GOPATH/github.com/cncd/pipeline/samples/sample_1
  2. # ll
  3. total 28
  4. drwxr-xr-x 2 root root 4096 Jan 22 11:44 ./
  5. drwxr-xr-x 13 root root 4096 Jan 22 11:02 ../
  6. -rw-r--r-- 1 root root 549 Jan 22 11:02 .env
  7. -rw-r--r-- 1 root root 6804 Jan 22 16:30 pipeline.json
  8. -rw-r--r-- 1 root root 229 Jan 22 11:02 pipeline.yml
  9. -rw-r--r-- 1 root root 138 Jan 22 11:02 README.md
  • pipeline.yml 定义文件
  • pipeline.json 编译后的配置文件
  • .env 环境变量

先来查看pipeline.yml 定义

  1. workspace:
  2. base: /go
  3. path: src/github.com/drone/envsubst
  4. clone:
  5. git:
  6. image: plugins/git
  7. depth: 50
  8. pipeline:
  9. build:
  10. image: golang:1.7
  11. commands:
  12. - go get -t ./...
  13. - go build
  14. - go test -v

上面的yml定义了:

  • 工作目录workspace
  • 初始化工作,git clone仓库,仓库地址在.env里定义
  • 然后是定义pipeline,
    • pipeline下面是step数组,这里只有一个build
    • 使用golang:1.7镜像
    • 构建命令在commands数组里定义

通过pipec compilecompile配置文件:

  1. # pipec compile
  2. Successfully compiled pipeline.yml to pipeline.json

查看编译后的pipeline.json

  1. {
  2. "pipeline": [
  3. {
  4. "name": "pipeline_clone_0",
  5. "alias": "git",
  6. "steps": [
  7. {
  8. "name": "pipeline_clone_0",
  9. "alias": "git",
  10. "image": "plugins/git:latest",
  11. "working_dir": "/go/src/github.com/drone/envsubst",
  12. "environment": {
  13. "CI": "drone",
  14. "CI_BUILD_CREATED": "1486119586",
  15. "CI_BUILD_EVENT": "push",
  16. "CI_BUILD_NUMBER": "6",
  17. "CI_BUILD_STARTED": "1486119585",
  18. "CI_COMMIT_AUTHOR": "bradrydzewski",
  19. "CI_COMMIT_AUTHOR_NAME": "bradrydzewski",
  20. "CI_COMMIT_BRANCH": "master",
  21. "CI_COMMIT_MESSAGE": "added a few more test cases for escaping behavior",
  22. "CI_COMMIT_REF": "refs/heads/master",
  23. "CI_COMMIT_SHA": "d0876d3176965f9552a611cbd56e24a9264355e6",
  24. "CI_REMOTE_URL": "https://github.com/drone/envsubst.git",
  25. "CI_REPO": "drone/envsubst",
  26. "CI_REPO_LINK": "https://github.com/drone/envsubst",
  27. "CI_REPO_NAME": "drone/envsubst",
  28. "CI_REPO_REMOTE": "https://github.com/drone/envsubst.git",
  29. "CI_SYSTEM": "pipec",
  30. "CI_SYSTEM_ARCH": "linux/amd64",
  31. "CI_SYSTEM_LINK": "https://github.com/cncd/pipec",
  32. "CI_SYSTEM_NAME": "pipec",
  33. "CI_WORKSPACE": "/go/src/github.com/drone/envsubst",
  34. "DRONE": "true",
  35. "DRONE_ARCH": "linux/amd64",
  36. "DRONE_BRANCH": "master",
  37. "DRONE_BUILD_CREATED": "1486119586",
  38. "DRONE_BUILD_EVENT": "push",
  39. "DRONE_BUILD_LINK": "https://github.com/cncd/pipec/drone/envsubst/6",
  40. "DRONE_BUILD_NUMBER": "6",
  41. "DRONE_BUILD_STARTED": "1486119585",
  42. "DRONE_COMMIT": "d0876d3176965f9552a611cbd56e24a9264355e6",
  43. "DRONE_COMMIT_AUTHOR": "bradrydzewski",
  44. "DRONE_COMMIT_BRANCH": "master",
  45. "DRONE_COMMIT_MESSAGE": "added a few more test cases for escaping behavior",
  46. "DRONE_COMMIT_REF": "refs/heads/master",
  47. "DRONE_COMMIT_SHA": "d0876d3176965f9552a611cbd56e24a9264355e6",
  48. "DRONE_JOB_STARTED": "1486119585",
  49. "DRONE_REMOTE_URL": "https://github.com/drone/envsubst.git",
  50. "DRONE_REPO": "drone/envsubst",
  51. "DRONE_REPO_LINK": "https://github.com/drone/envsubst",
  52. "DRONE_REPO_NAME": "envsubst",
  53. "DRONE_REPO_OWNER": "drone",
  54. "DRONE_REPO_SCM": "git",
  55. "DRONE_WORKSPACE": "/go/src/github.com/drone/envsubst",
  56. "PLUGIN_DEPTH": "50"
  57. },
  58. "volumes": [
  59. "pipeline_default:/go"
  60. ],
  61. "networks": [
  62. {
  63. "name": "pipeline_default",
  64. "aliases": [
  65. "git"
  66. ]
  67. }
  68. ],
  69. "on_success": true,
  70. "auth_config": {}
  71. }
  72. ]
  73. },
  74. {
  75. "name": "pipeline_stage_0",
  76. "alias": "build",
  77. "steps": [
  78. {
  79. "name": "pipeline_step_0",
  80. "alias": "build",
  81. "image": "golang:1.7",
  82. "working_dir": "/go/src/github.com/drone/envsubst",
  83. "environment": {
  84. "CI": "drone",
  85. "CI_BUILD_CREATED": "1486119586",
  86. "CI_BUILD_EVENT": "push",
  87. "CI_BUILD_NUMBER": "6",
  88. "CI_BUILD_STARTED": "1486119585",
  89. "CI_COMMIT_AUTHOR": "bradrydzewski",
  90. "CI_COMMIT_AUTHOR_NAME": "bradrydzewski",
  91. "CI_COMMIT_BRANCH": "master",
  92. "CI_COMMIT_MESSAGE": "added a few more test cases for escaping behavior",
  93. "CI_COMMIT_REF": "refs/heads/master",
  94. "CI_COMMIT_SHA": "d0876d3176965f9552a611cbd56e24a9264355e6",
  95. "CI_REMOTE_URL": "https://github.com/drone/envsubst.git",
  96. "CI_REPO": "drone/envsubst",
  97. "CI_REPO_LINK": "https://github.com/drone/envsubst",
  98. "CI_REPO_NAME": "drone/envsubst",
  99. "CI_REPO_REMOTE": "https://github.com/drone/envsubst.git",
  100. "CI_SCRIPT": "CmlmIFsgLW4gIiRDSV9ORVRSQ19NQUNISU5FIiBdOyB0aGVuCmNhdCA8PEVPRiA+ICRIT01FLy5uZXRyYwptYWNoaW5lICRDSV9ORVRSQ19NQUNISU5FCmxvZ2luICRDSV9ORVRSQ19VU0VSTkFNRQpwYXNzd29yZCAkQ0lfTkVUUkNfUEFTU1dPUkQKRU9GCmNobW9kIDA2MDAgJEhPTUUvLm5ldHJjCmZpCnVuc2V0IENJX05FVFJDX1VTRVJOQU1FCnVuc2V0IENJX05FVFJDX1BBU1NXT1JECnVuc2V0IENJX1NDUklQVAp1bnNldCBEUk9ORV9ORVRSQ19VU0VSTkFNRQp1bnNldCBEUk9ORV9ORVRSQ19QQVNTV09SRAoKZWNobyArICJnbyBnZXQgLXQgLi8uLi4iCmdvIGdldCAtdCAuLy4uLgoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQKCmVjaG8gKyAiZ28gdGVzdCAtdiIKZ28gdGVzdCAtdgoK",
  101. "CI_SYSTEM": "pipec",
  102. "CI_SYSTEM_ARCH": "linux/amd64",
  103. "CI_SYSTEM_LINK": "https://github.com/cncd/pipec",
  104. "CI_SYSTEM_NAME": "pipec",
  105. "CI_WORKSPACE": "/go/src/github.com/drone/envsubst",
  106. "DRONE": "true",
  107. "DRONE_ARCH": "linux/amd64",
  108. "DRONE_BRANCH": "master",
  109. "DRONE_BUILD_CREATED": "1486119586",
  110. "DRONE_BUILD_EVENT": "push",
  111. "DRONE_BUILD_LINK": "https://github.com/cncd/pipec/drone/envsubst/6",
  112. "DRONE_BUILD_NUMBER": "6",
  113. "DRONE_BUILD_STARTED": "1486119585",
  114. "DRONE_COMMIT": "d0876d3176965f9552a611cbd56e24a9264355e6",
  115. "DRONE_COMMIT_AUTHOR": "bradrydzewski",
  116. "DRONE_COMMIT_BRANCH": "master",
  117. "DRONE_COMMIT_MESSAGE": "added a few more test cases for escaping behavior",
  118. "DRONE_COMMIT_REF": "refs/heads/master",
  119. "DRONE_COMMIT_SHA": "d0876d3176965f9552a611cbd56e24a9264355e6",
  120. "DRONE_JOB_STARTED": "1486119585",
  121. "DRONE_REMOTE_URL": "https://github.com/drone/envsubst.git",
  122. "DRONE_REPO": "drone/envsubst",
  123. "DRONE_REPO_LINK": "https://github.com/drone/envsubst",
  124. "DRONE_REPO_NAME": "envsubst",
  125. "DRONE_REPO_OWNER": "drone",
  126. "DRONE_REPO_SCM": "git",
  127. "DRONE_WORKSPACE": "/go/src/github.com/drone/envsubst",
  128. "HOME": "/root",
  129. "SHELL": "/bin/sh"
  130. },
  131. "entrypoint": [
  132. "/bin/sh",
  133. "-c"
  134. ],
  135. "command": [
  136. "echo $CI_SCRIPT | base64 -d | /bin/sh -e"
  137. ],
  138. "volumes": [
  139. "pipeline_default:/go"
  140. ],
  141. "networks": [
  142. {
  143. "name": "pipeline_default",
  144. "aliases": [
  145. "build"
  146. ]
  147. }
  148. ],
  149. "on_success": true,
  150. "auth_config": {}
  151. }
  152. ]
  153. }
  154. ],
  155. "networks": [
  156. {
  157. "name": "pipeline_default",
  158. "driver": "bridge"
  159. }
  160. ],
  161. "volumes": [
  162. {
  163. "name": "pipeline_default",
  164. "driver": "local"
  165. }
  166. ],
  167. "secrets": null
  168. }

简单分析结构:

  • pipeline 定义了执行的stage,每个stage有一个或者多个step
  • networks、volumes、secrets 分别定义网络、存储和secrets
    • 通过network,实现container互通
    • 通过volumes实现数据共享

最后执行,通过pipec exec

  1. # pipec exec
  2. proc "pipeline_clone_0" started
  3. + git init
  4. Initialized empty Git repository in /go/src/github.com/drone/envsubst/.git/
  5. + git remote add origin https://github.com/drone/envsubst.git
  6. + git fetch --no-tags --depth=50 origin +refs/heads/master:
  7. From https://github.com/drone/envsubst
  8. * branch master -> FETCH_HEAD
  9. * [new branch] master -> origin/master
  10. + git reset --hard -q d0876d3176965f9552a611cbd56e24a9264355e6
  11. + git submodule update --init --recursive
  12. proc "pipeline_clone_0" exited with status 0
  13. proc "pipeline_step_0" started
  14. + go get -t ./...
  15. + go build
  16. + go test -v
  17. === RUN TestExpand
  18. --- PASS: TestExpand (0.00s)
  19. === RUN TestFuzz
  20. --- PASS: TestFuzz (0.01s)
  21. === RUN Test_len
  22. --- PASS: Test_len (0.00s)
  23. === RUN Test_lower
  24. --- PASS: Test_lower (0.00s)
  25. === RUN Test_lowerFirst
  26. --- PASS: Test_lowerFirst (0.00s)
  27. === RUN Test_upper
  28. --- PASS: Test_upper (0.00s)
  29. === RUN Test_upperFirst
  30. --- PASS: Test_upperFirst (0.00s)
  31. === RUN Test_default
  32. --- PASS: Test_default (0.00s)
  33. PASS
  34. ok github.com/drone/envsubst 0.009s
  35. proc "pipeline_step_0" exited with status 0

pipeline 原理分析

编译过程

可以形象的理解为 .env+pipeline.yml --> pipeline.json

编译过程不复杂,主要是解析pipeline.yml为Config:

  1. Config struct {
  2. Cache libcompose.Stringorslice
  3. Platform string
  4. Branches Constraint
  5. Workspace Workspace
  6. Clone Containers
  7. Pipeline Containers
  8. Services Containers
  9. Networks Networks
  10. Volumes Volumes
  11. Labels libcompose.SliceorMap
  12. }

然后转换为json对应的config:

  1. Config struct {
  2. Stages []*Stage `json:"pipeline"` // pipeline stages
  3. Networks []*Network `json:"networks"` // network definitions
  4. Volumes []*Volume `json:"volumes"` // volume definitions
  5. Secrets []*Secret `json:"secrets"` // secret definitions
  6. }

该部分主要代码在pipeline/frontend里

执行过程

我们主要关注执行过程,主要代码在pipeline/backend里。

首先是读取配置文件为backend.Config

  1. config, err := pipeline.Parse(reader)
  2. if err != nil {
  3. return err
  4. }

然后创建执行环境,目前的代码仅docker可用,k8s是空代码。

  1. var engine backend.Engine
  2. if c.Bool("kubernetes") {
  3. engine = kubernetes.New(
  4. c.String("kubernetes-namepsace"),
  5. c.String("kubernetes-endpoint"),
  6. c.String("kubernetes-token"),
  7. )
  8. } else {
  9. engine, err = docker.NewEnv()
  10. if err != nil {
  11. return err
  12. }
  13. }

接着开始执行

  1. ctx, cancel := context.WithTimeout(context.Background(), c.Duration("timeout"))
  2. defer cancel()
  3. ctx = interrupt.WithContext(ctx)
  4. return pipeline.New(config,
  5. pipeline.WithContext(ctx),
  6. pipeline.WithLogger(defaultLogger),
  7. pipeline.WithTracer(defaultTracer),
  8. pipeline.WithEngine(engine),
  9. ).Run()

其中pipeline.NEW创建了Runtime对象;

  1. type Runtime struct {
  2. err error // 错误信息
  3. spec *backend.Config // 配置信息
  4. engine backend.Engine // docker engine
  5. started int64 // 开始时间
  6. ctx context.Context
  7. tracer Tracer
  8. logger Logger
  9. }

其中Engine,操作容器的interface,目前仅docker可用。

  1. // Engine defines a container orchestration backend and is used
  2. // to create and manage container resources.
  3. type Engine interface {
  4. // Setup the pipeline environment.
  5. Setup(context.Context, *Config) error
  6. // Start the pipeline step.
  7. Exec(context.Context, *Step) error
  8. // Kill the pipeline step.
  9. Kill(context.Context, *Step) error
  10. // Wait for the pipeline step to complete and returns
  11. // the completion results.
  12. Wait(context.Context, *Step) (*State, error)
  13. // Tail the pipeline step logs.
  14. Tail(context.Context, *Step) (io.ReadCloser, error)
  15. // Destroy the pipeline environment.
  16. Destroy(context.Context, *Config) error
  17. }

关注Run:

  1. // Run starts the runtime and waits for it to complete.
  2. func (r *Runtime) Run() error {
  3. // 延迟函数,用于销毁docker env
  4. defer func() {
  5. r.engine.Destroy(r.ctx, r.spec)
  6. }()
  7. // 初始化docker engine
  8. r.started = time.Now().Unix()
  9. if err := r.engine.Setup(r.ctx, r.spec); err != nil {
  10. return err
  11. }
  12. // 依次运行stage
  13. for _, stage := range r.spec.Stages {
  14. select {
  15. case <-r.ctx.Done():
  16. return ErrCancel
  17. // 执行
  18. case err := <-r.execAll(stage.Steps):
  19. if err != nil {
  20. r.err = err
  21. }
  22. }
  23. }
  24. return r.err
  25. }

重点在于使用errgroup.Group通过协程方式运行step:


  1. // 执行所有steps
  2. func (r *Runtime) execAll(procs []*backend.Step) <-chan error {
  3. var g errgroup.Group
  4. done := make(chan error)
  5. // 遍历执行step
  6. for _, proc := range procs {
  7. // 协程 exec
  8. proc := proc
  9. g.Go(func() error {
  10. return r.exec(proc)
  11. })
  12. }
  13. go func() {
  14. done <- g.Wait()
  15. close(done)
  16. }()
  17. return done
  18. }
  19. // 执行单个step
  20. func (r *Runtime) exec(proc *backend.Step) error {
  21. switch {
  22. case r.err != nil && proc.OnFailure == false:
  23. return nil
  24. case r.err == nil && proc.OnSuccess == false:
  25. return nil
  26. }
  27. // trace日志
  28. if r.tracer != nil {
  29. state := new(State)
  30. state.Pipeline.Time = r.started
  31. state.Pipeline.Error = r.err
  32. state.Pipeline.Step = proc
  33. state.Process = new(backend.State) // empty
  34. if err := r.tracer.Trace(state); err == ErrSkip {
  35. return nil
  36. } else if err != nil {
  37. return err
  38. }
  39. }
  40. // docker engine执行
  41. if err := r.engine.Exec(r.ctx, proc); err != nil {
  42. return err
  43. }
  44. // 记录日志信息
  45. if r.logger != nil {
  46. rc, err := r.engine.Tail(r.ctx, proc)
  47. if err != nil {
  48. return err
  49. }
  50. go func() {
  51. r.logger.Log(proc, multipart.New(rc))
  52. rc.Close()
  53. }()
  54. }
  55. if proc.Detached {
  56. return nil
  57. }
  58. // 等待docker engine执行完成
  59. wait, err := r.engine.Wait(r.ctx, proc)
  60. if err != nil {
  61. return err
  62. }
  63. if r.tracer != nil {
  64. state := new(State)
  65. state.Pipeline.Time = r.started
  66. state.Pipeline.Error = r.err
  67. state.Pipeline.Step = proc
  68. state.Process = wait
  69. if err := r.tracer.Trace(state); err != nil {
  70. return err
  71. }
  72. }
  73. if wait.OOMKilled {
  74. return &OomError{
  75. Name: proc.Name,
  76. Code: wait.ExitCode,
  77. }
  78. } else if wait.ExitCode != 0 {
  79. return &ExitError{
  80. Name: proc.Name,
  81. Code: wait.ExitCode,
  82. }
  83. }
  84. return nil
  85. }

作者:Jadepeng

出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi

您的支持是对博主最大的鼓励,感谢您的认真阅读。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

drone的pipeline原理与代码分析的更多相关文章

  1. 免费的Lucene 原理与代码分析完整版下载

    Lucene是一个基于Java的高效的全文检索库.那么什么是全文检索,为什么需要全文检索?目前人们生活中出现的数据总的来说分为两类:结构化数据和非结构化数据.很容易理解,结构化数据是有固定格式和结构的 ...

  2. OpenStack 虚拟机冷/热迁移的实现原理与代码分析

    目录 文章目录 目录 前文列表 冷迁移代码分析(基于 Newton) Nova 冷迁移实现原理 热迁移代码分析 Nova 热迁移实现原理 向 libvirtd 发出 Live Migration 指令 ...

  3. SQL注入原理及代码分析(二)

    前言 上一篇文章中,对union注入.报错注入.布尔盲注等进行了分析,接下来这篇文章,会对堆叠注入.宽字节注入.cookie注入等进行分析.第一篇文章地址:SQL注入原理及代码分析(一) 如果想要了解 ...

  4. XSS原理及代码分析

    前言 XSS又叫跨站脚本攻击,是一种对网站应用程序的安全漏洞攻击技术.它允许恶意用户将代码注入网页,其他用户在浏览网页时就会受到影响.XSS分为三种:反射型,存储型,和DOM型.下面我会构造有缺陷的代 ...

  5. lighttpd与fastcgi+cgilua原理、代码分析与安装

    原理 http://www.cnblogs.com/skynet/p/4173450.html 快速通用网关接口(Fast Common Gateway Interface/FastCGI)是通用网关 ...

  6. SQL注入原理及代码分析(一)

    前言 我们都知道,学安全,懂SQL注入是重中之重,因为即使是现在SQL注入漏洞依然存在,只是相对于之前现在挖SQL注入变的困难了.而且知识点比较多,所以在这里总结一下.通过构造有缺陷的代码,来理解常见 ...

  7. AbstractQueuedSynchronizer原理及代码分析

    一.AQS简介 AbstractQueuedSynchronizer(AQS)是java.util.concurrent并发包下最基本的同步器,其它同步器实现,如ReentrantLock类,Reen ...

  8. WordPress HOOK机制原理及代码分析

    WordPress强大的插件机制让我们可以自由扩展功能.网上对插件的使用以及开发方法都有大量资料可以查询. 今天我们就分析一下四个主要函数的代码,包括: add_action.do_action.ad ...

  9. Openvswitch原理与代码分析(6):用户态流表flow table的操作

    当内核无法查找到流表项的时候,则会通过upcall来调用用户态ovs-vswtichd中的flow table. 会调用ofproto-dpif-upcall.c中的udpif_upcall_hand ...

随机推荐

  1. 其他-n个互相独立的连续随机变量中第i小的数值期望

    提出问题 有\(n\)个互相独立的\(0\)至\(1\)之间等概率生成的随机变量,求从小到大排序后第\(i\)个数的数值期望 一个简化的问题 我们先来求解一个简化的问题:最大值的数值期望是多少? 我们 ...

  2. queue之#单向消息队列

    import queue q = queue.Queue() #创建一个单项队列qsize 查看这个单项队列元素的个数empty 与 clear功能是一样的full 是用来查看这个队列是否填满了,队列 ...

  3. linux下.bashrc文件 /PATH环境变量修改 /提示符修改

    1) .bashrc文件 在linux系统普通用户目录(cd /home/xxx)或root用户目录(cd /root)下,用指令ls -al可以看到4个隐藏文件, .bash_history   记 ...

  4. Zabbix3.2监控Windows的内存使用百分比并在内存使用率超过85%的时候触发报警

    内存使用率key:vm.memory.size[pused]

  5. LabVIEW 获取本机多个ip地址

    图 1   网上见了好多设置的,都没讲清楚,在这里整理一下本机ip地址的获取问题.关键在"字符串向ip地址转换"函数的设置上面,见下图2,选择多输出就能获取本机的多个ip地址,若不 ...

  6. 本地项目提交到github和提交更新(转)

    一:首先当然是去github注册账号了. 二:注册完毕登录后,在自己的首页上面点击右上角“+”号,然后选择New repository,或者直接点击下面的绿色按钮,创建一个新仓库.如图: 然后填入仓库 ...

  7. [POSIX]文件系统(概述)

    1.文件名由除系统目录分隔符(unix是/,windows是\)和空字符“\0”外的任意ASCII字符组成,现代系统很多还可以包含UNICODE字符,但是还是推荐使用传统的ASCII码命名. 2.目录 ...

  8. Android Apk 瘦身大法

    原文地址: https://mp.weixin.qq.com/s/XS0tuLgTfyp4rW4h69wyQQ 一, 我们在多人开发项目 或者 遗留项目中开发时,会有些自己没用到的资源文件,但是自己也 ...

  9. CodeCraft-19 and Codeforces Round #537 (Div. 2) 题解

    传送门 D. Destroy the Colony 首先明确题意:除了规定的两种(或一种)字母要在同侧以外,其他字母也必须在同侧. 发现当每种字母在左/右边确定之后,方案数就确定了,就是分组的方案数乘 ...

  10. wx :swipertab切换

    <view> <view class="navbar"> <block wx:for="{{body}}" wx:key=&quo ...