├── .gitignore
├── CNAME
├── LICENSE
├── README.md
├── assets
├── custom.js
├── zino-app-arch.png
├── zino-app-arch.svg
├── zino-logo.png
├── zino-logo.svg
└── zino-web-wechat.png
├── book.toml
├── deploy.sh
├── src
├── SUMMARY.md
├── basics.md
├── basics
│ ├── application.md
│ ├── error.md
│ ├── model.md
│ ├── request.md
│ ├── response.md
│ ├── schedule.md
│ └── state.md
├── guide.md
├── guide
│ ├── config.md
│ ├── deployment.md
│ ├── development.md
│ ├── new-app.md
│ └── structure.md
├── integrations.md
├── integrations
│ ├── actix-web.md
│ ├── axum.md
│ └── dioxus.md
├── introduction.md
├── orm.md
├── orm
│ ├── default-controller.md
│ ├── openapi.md
│ ├── table-mapping.md
│ └── trait-derive.md
├── resources.md
├── topics.md
└── topics
│ ├── accessor.md
│ ├── authentication.md
│ ├── cache.md
│ ├── channel.md
│ ├── chatbot.md
│ ├── connector.md
│ ├── datetime.md
│ ├── extension.md
│ ├── i18n.md
│ ├── observability.md
│ └── view.md
└── theme
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | book
2 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | zino.cc
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Legal Code
2 |
3 | CC0 1.0 Universal
4 |
5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12 | HEREUNDER.
13 |
14 | Statement of Purpose
15 |
16 | The laws of most jurisdictions throughout the world automatically confer
17 | exclusive Copyright and Related Rights (defined below) upon the creator
18 | and subsequent owner(s) (each and all, an "owner") of an original work of
19 | authorship and/or a database (each, a "Work").
20 |
21 | Certain owners wish to permanently relinquish those rights to a Work for
22 | the purpose of contributing to a commons of creative, cultural and
23 | scientific works ("Commons") that the public can reliably and without fear
24 | of later claims of infringement build upon, modify, incorporate in other
25 | works, reuse and redistribute as freely as possible in any form whatsoever
26 | and for any purposes, including without limitation commercial purposes.
27 | These owners may contribute to the Commons to promote the ideal of a free
28 | culture and the further production of creative, cultural and scientific
29 | works, or to gain reputation or greater distribution for their Work in
30 | part through the use and efforts of others.
31 |
32 | For these and/or other purposes and motivations, and without any
33 | expectation of additional consideration or compensation, the person
34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35 | is an owner of Copyright and Related Rights in the Work, voluntarily
36 | elects to apply CC0 to the Work and publicly distribute the Work under its
37 | terms, with knowledge of his or her Copyright and Related Rights in the
38 | Work and the meaning and intended legal effect of CC0 on those rights.
39 |
40 | 1. Copyright and Related Rights. A Work made available under CC0 may be
41 | protected by copyright and related or neighboring rights ("Copyright and
42 | Related Rights"). Copyright and Related Rights include, but are not
43 | limited to, the following:
44 |
45 | i. the right to reproduce, adapt, distribute, perform, display,
46 | communicate, and translate a Work;
47 | ii. moral rights retained by the original author(s) and/or performer(s);
48 | iii. publicity and privacy rights pertaining to a person's image or
49 | likeness depicted in a Work;
50 | iv. rights protecting against unfair competition in regards to a Work,
51 | subject to the limitations in paragraph 4(a), below;
52 | v. rights protecting the extraction, dissemination, use and reuse of data
53 | in a Work;
54 | vi. database rights (such as those arising under Directive 96/9/EC of the
55 | European Parliament and of the Council of 11 March 1996 on the legal
56 | protection of databases, and under any national implementation
57 | thereof, including any amended or successor version of such
58 | directive); and
59 | vii. other similar, equivalent or corresponding rights throughout the
60 | world based on applicable law or treaty, and any national
61 | implementations thereof.
62 |
63 | 2. Waiver. To the greatest extent permitted by, but not in contravention
64 | of, applicable law, Affirmer hereby overtly, fully, permanently,
65 | irrevocably and unconditionally waives, abandons, and surrenders all of
66 | Affirmer's Copyright and Related Rights and associated claims and causes
67 | of action, whether now known or unknown (including existing as well as
68 | future claims and causes of action), in the Work (i) in all territories
69 | worldwide, (ii) for the maximum duration provided by applicable law or
70 | treaty (including future time extensions), (iii) in any current or future
71 | medium and for any number of copies, and (iv) for any purpose whatsoever,
72 | including without limitation commercial, advertising or promotional
73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74 | member of the public at large and to the detriment of Affirmer's heirs and
75 | successors, fully intending that such Waiver shall not be subject to
76 | revocation, rescission, cancellation, termination, or any other legal or
77 | equitable action to disrupt the quiet enjoyment of the Work by the public
78 | as contemplated by Affirmer's express Statement of Purpose.
79 |
80 | 3. Public License Fallback. Should any part of the Waiver for any reason
81 | be judged legally invalid or ineffective under applicable law, then the
82 | Waiver shall be preserved to the maximum extent permitted taking into
83 | account Affirmer's express Statement of Purpose. In addition, to the
84 | extent the Waiver is so judged Affirmer hereby grants to each affected
85 | person a royalty-free, non transferable, non sublicensable, non exclusive,
86 | irrevocable and unconditional license to exercise Affirmer's Copyright and
87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
88 | maximum duration provided by applicable law or treaty (including future
89 | time extensions), (iii) in any current or future medium and for any number
90 | of copies, and (iv) for any purpose whatsoever, including without
91 | limitation commercial, advertising or promotional purposes (the
92 | "License"). The License shall be deemed effective as of the date CC0 was
93 | applied by Affirmer to the Work. Should any part of the License for any
94 | reason be judged legally invalid or ineffective under applicable law, such
95 | partial invalidity or ineffectiveness shall not invalidate the remainder
96 | of the License, and in such case Affirmer hereby affirms that he or she
97 | will not (i) exercise any of his or her remaining Copyright and Related
98 | Rights in the Work or (ii) assert any associated claims and causes of
99 | action with respect to the Work, in either case contrary to Affirmer's
100 | express Statement of Purpose.
101 |
102 | 4. Limitations and Disclaimers.
103 |
104 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
105 | surrendered, licensed or otherwise affected by this document.
106 | b. Affirmer offers the Work as-is and makes no representations or
107 | warranties of any kind concerning the Work, express, implied,
108 | statutory or otherwise, including without limitation warranties of
109 | title, merchantability, fitness for a particular purpose, non
110 | infringement, or the absence of latent or other defects, accuracy, or
111 | the present or absence of errors, whether or not discoverable, all to
112 | the greatest extent permissible under applicable law.
113 | c. Affirmer disclaims responsibility for clearing rights of other persons
114 | that may apply to the Work or any use thereof, including without
115 | limitation any person's Copyright and Related Rights in the Work.
116 | Further, Affirmer disclaims responsibility for obtaining any necessary
117 | consents, permissions or other rights required for any use of the
118 | Work.
119 | d. Affirmer understands and acknowledges that Creative Commons is not a
120 | party to this document and has no duty or obligation with respect to
121 | this CC0 or use of the Work.
122 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # zino-docs-zh
2 |
3 | Zino开发框架中文教程
4 |
5 | - [引言](./src/introduction.md)
6 | - [入门指南](./src/guide.md)
7 | - [创建应用](./src/guide/new-app.md)
8 | - [目录结构](./src/guide/structure.md)
9 | - [配置文件](./src/guide/config.md)
10 | - [基础模块](./src/basics.md)
11 | - [应用接口抽象](./src/basics/application.md)
12 | - [状态管理](./src/basics/state.md)
13 | - [错误处理](./src/basics/error.md)
14 | - [请求上下文(TODO)](./src/basics/request.md)
15 | - [请求响应(TODO)](./src/basics/response.md)
16 | - [任务调度(TODO)](./src/basics/schedule.md)
17 | - [领域模型(TODO)](./src/basics/model.md)
18 | - [数据库ORM(TODO)](./src/orm.md)
19 | - [数据表映射(TODO)](./src/orm/table-mapping.md)
20 | - [Trait推导(TODO)](./src/orm/trait-derive.md)
21 | - [API自动生成(TODO)](./src/orm/default-controller.md)
22 | - [OpenAPI文档(TODO)](./src/orm/openapi.md)
23 | - [进阶专题(TODO)](./src/topics.md)
24 | - [身份认证(TODO)](./src/topics/authentication.md)
25 | - [可观测性(TODO)](./src/topics/observability.md)
26 | - [拓展机制(TODO)](./src/topics/extension.md)
27 | - [框架集成(TODO)](./src/integrations.md)
28 | - [actix-web(TODO)](./src/integrations/actix-web.md)
29 | - [axum(TODO)](./src/integrations/axum.md)
30 | - [dioxus(TODO)](./src/integrations/dioxus.md)
31 | - [更多资源](./src/resources.md)
32 |
--------------------------------------------------------------------------------
/assets/custom.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('load', () => {
2 | const script = document.createElement('script');
3 | script.type = 'text/javascript';
4 | script.src = 'https://giscus.app/client.js';
5 | script.async = true;
6 | script.crossOrigin = 'anonymous';
7 | script.setAttribute('data-repo', 'zino-rs/zino-docs-zh');
8 | script.setAttribute('data-repo-id', 'R_kgDOJZESbg');
9 | script.setAttribute('data-category', 'Announcements');
10 | script.setAttribute('data-category-id', 'DIC_kwDOJZESbs4CecQK');
11 | script.setAttribute('data-mapping', 'title');
12 | script.setAttribute('data-script', '0');
13 | script.setAttribute('data-reactions-enabled', '1');
14 | script.setAttribute('data-emit-metadata', '0');
15 | script.setAttribute('data-input-position', 'top');
16 | script.setAttribute('data-theme', 'preferred_color_scheme');
17 | script.setAttribute('data-lang', 'zh-CN');
18 | script.setAttribute('data-loading', 'lazy');
19 | document.getElementById('content').appendChild(script);
20 | });
--------------------------------------------------------------------------------
/assets/zino-app-arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zino-rs/zino-docs-zh/20f1a0a405c1defb5abff7ee3b1e1c4253b8bbc3/assets/zino-app-arch.png
--------------------------------------------------------------------------------
/assets/zino-app-arch.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/assets/zino-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zino-rs/zino-docs-zh/20f1a0a405c1defb5abff7ee3b1e1c4253b8bbc3/assets/zino-logo.png
--------------------------------------------------------------------------------
/assets/zino-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/zino-web-wechat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zino-rs/zino-docs-zh/20f1a0a405c1defb5abff7ee3b1e1c4253b8bbc3/assets/zino-web-wechat.png
--------------------------------------------------------------------------------
/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | title = "Zino开发框架中文教程"
3 | authors = ["photino"]
4 | language = "zh"
5 | multilingual = false
6 | src = "src"
7 |
8 | [rust]
9 | edition = "2021"
10 |
11 | [output.html]
12 | default-theme = "light"
13 | additional-css = ["theme/style.css"]
14 | additional-js = ["assets/custom.js"]
15 | git-repository-url = "https://github.com/zino-rs/zino-docs-zh"
16 | edit-url-template = "https://github.com/zino-rs/zino-docs-zh/edit/main/{path}"
17 | cname = "zino.cc"
18 |
19 | [output.html.playground]
20 | editable = false
21 | copyable = true
22 | copy-js = true
23 | line-numbers = true
24 | runnable = false
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | ## build static website for book
2 | mdbook build
3 |
4 | ## init git repo
5 | cd book
6 | cp ../CNAME ./
7 | mkdir assets
8 | cp ../assets/** assets/
9 | git init
10 | git config user.name "photino"
11 | git config user.email "panzan89@163.com"
12 | git add .
13 | git commit -m 'deploy'
14 | git branch -M gh-pages
15 | git remote add origin https://github.com/zino-rs/zino-docs-zh
16 |
17 | ## push to github pages
18 | git push -u -f origin gh-pages
--------------------------------------------------------------------------------
/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | - [引言](./introduction.md)
4 | - [入门指南](./guide.md)
5 | - [创建应用](./guide/new-app.md)
6 | - [目录结构](./guide/structure.md)
7 | - [配置文件](./guide/config.md)
8 | - [基础模块](./basics.md)
9 | - [应用接口抽象](./basics/application.md)
10 | - [状态管理](./basics/state.md)
11 | - [错误处理](./basics/error.md)
12 | - [请求上下文](./basics/request.md)
13 | - [请求响应(TODO)]()
14 | - [任务调度(TODO)]()
15 | - [领域模型(TODO)]()
16 | - [数据库ORM(TODO)]()
17 | - [数据表映射(TODO)]()
18 | - [Trait推导(TODO)]()
19 | - [API自动生成(TODO)]()
20 | - [OpenAPI文档(TODO)]()
21 | - [进阶专题(TODO)]()
22 | - [身份认证(TODO)]()
23 | - [可观测性(TODO)]()
24 | - [拓展机制(TODO)]()
25 | - [框架集成(TODO)]()
26 | - [actix-web(TODO)]()
27 | - [axum(TODO)]()
28 | - [dioxus(TODO)]()
29 | - [更多资源](./resources.md)
30 |
--------------------------------------------------------------------------------
/src/basics.md:
--------------------------------------------------------------------------------
1 | # 基础模块
2 |
3 | Zino框架的主要功能都由[`zino-core`]提供,这是与具体Web框架(如[`actix-web`]或[`axum`])无关的抽象,
4 | 其中最基础的就是以下七大模块:
5 |
6 | - [`application`]:应用接口抽象,这是与其他框架差异最大的地方;
7 | - [`state`]:应用状态管理,大量使用惰性初始化的全局变量;
8 | - [`error`]:通用的错误处理,搭配[`bail!`]、[`warn!`]、[`reject!`]使用;
9 | - [`request`]:请求上下文,通过[`RequestContext`] trait提供方法;
10 | - [`response`]:构建请求响应,统一处理不同的`content-type`;
11 | - [`schedule`]:任务调度,提供[`Scheduler`]、[`AsyncScheduler`]抽象;
12 | - [`model`]:领域模型抽象,与具体的数据库和ORM无关。
13 |
14 | [`zino-core`]: https://docs.rs/zino-core
15 | [`actix-web`]: https://docs.rs/actix-web
16 | [`axum`]: https://docs.rs/axum
17 | [`application`]: https://docs.rs/zino-core/latest/zino_core/application/index.html
18 | [`state`]: https://docs.rs/zino-core/latest/zino_core/state/index.html
19 | [`error`]: https://docs.rs/zino-core/latest/zino_core/error/index.html
20 | [`request`]: https://docs.rs/zino-core/latest/zino_core/request/index.html
21 | [`response`]: https://docs.rs/zino-core/latest/zino_core/response/index.html
22 | [`schedule`]: https://docs.rs/zino-core/latest/zino_core/schedule/index.html
23 | [`model`]: https://docs.rs/zino-core/latest/zino_core/model/index.html
24 | [`bail!`]: https://docs.rs/zino-core/latest/zino_core/macro.bail.html
25 | [`warn!`]: https://docs.rs/zino-core/latest/zino_core/macro.warn.html
26 | [`reject!`]: https://docs.rs/zino-core/latest/zino_core/macro.reject.html
27 | [`RequestContext`]: https://docs.rs/zino-core/latest/zino_core/request/trait.RequestContext.html
28 | [`Scheduler`]: https://docs.rs/zino-core/latest/zino_core/schedule/trait.Scheduler.html
29 | [`AsyncScheduler`]: https://docs.rs/zino-core/latest/zino_core/schedule/trait.AsyncScheduler.html
30 |
--------------------------------------------------------------------------------
/src/basics/application.md:
--------------------------------------------------------------------------------
1 | # 应用接口抽象
2 |
3 | Zino框架的应用接口抽象由[`Application`] trait定义,它包含一个关联类型和两个必须实现的方法:
4 |
5 | ```rust
6 | pub trait Application {
7 | type Routes;
8 |
9 | fn register(self, routes: Self::Routes) -> Self;
10 | fn run_with(self, scheduler: T);
11 | }
12 | ```
13 | 其中[`register`]用来注册路由,[`run_with`]用来加载异步任务并运行应用。
14 | 需要注意的是,异步任务的执行涉及到异步运行时的选择,
15 | 而[`zino-core`]本身并没有限定只能使用特定的运行时[^runtime],
16 | 所以需要实现者自行在[`run_with`]方法的实现中指定。对于同步任务,不涉及到异步运行时的选择,
17 | 我们就在[`Application`]的[`spawn`]方法中提供了默认实现。
18 |
19 | 这就是Zino框架的起点!我们只要给其他Web框架实现这个trait,就能把这个框架的功能集成到Zino中,并使应用的启动方式保存一致:
20 | ```rust
21 | mod router;
22 | mod schedule;
23 |
24 | use zino::prelude::*;
25 |
26 | fn main() {
27 | zino::Cluster::boot()
28 | .register(router::routes())
29 | .register_debug(router::debug_routes())
30 | .spawn(schedule::job_scheduler())
31 | .run_with(schedule::async_job_scheduler())
32 | }
33 | ```
34 |
35 | 目前我们已经为[`actix-web`]、[`axum`]、[`dioxus-desktop`]实现了[`Application`] trait,
36 | 它们对应的关联类型[`Routes`]分别为:
37 |
38 | - [`actix-web`]:引入`ActixCluster`类型,基于[`ServiceConfig`]来定义路由。
39 |
40 | ```rust
41 | pub type RouterConfigure = fn(cfg: &mut actix_web::web::ServiceConfig);
42 |
43 | impl Application for ActixCluster {
44 | type Routes = Vec;
45 | }
46 | ```
47 |
48 | - [`axum`]:引入`AxumCluster`类型,基于[`Router`]来定义路由。
49 |
50 | ```rust
51 | impl Application for AxumCluster {
52 | type Routes = Vec;
53 | }
54 | ```
55 |
56 | - [`dioxus-desktop`]:引入`DioxusDesktop`类型,基于[`Routable`]泛型约束来定义路由。
57 |
58 | ```rust
59 | impl Application for DioxusDesktop
60 | where
61 | R: dioxus_router::routable::Routable,
62 | ::Err: Display,
63 | {
64 | type Routes = R;
65 | }
66 | ```
67 |
68 | 可以看到,在以上框架的[`Application`]实现中,我们并没有定义自己的路由类型,
69 | 这就使得[`actix-web`]和[`axum`]中的路由、中间件可以直接在我们的Zino框架中使用。
70 | 确保充分理解了这一点,对我们的应用开发至关重要。
71 |
72 | [^runtime]: 虽然大部分情况下我们还是会优先选择[`tokio`]。
73 |
74 | [`zino-core`]: https://docs.rs/zino-core
75 | [`tokio`]: https://docs.rs/tokio
76 | [`actix-web`]: https://crates.io/crates/actix-web
77 | [`axum`]: https://crates.io/crates/axum
78 | [`dioxus-desktop`]: https://crates.io/crates/dioxus-desktop
79 | [`dioxus-router`]: https://crates.io/crates/dioxus-router
80 | [`Application`]: https://docs.rs/zino-core/latest/zino_core/application/trait.Application.html
81 | [`Routes`]: https://docs.rs/zino-core/latest/zino_core/application/trait.Application.html#associatedtype.Routes
82 | [`register`]: https://docs.rs/zino-core/latest/zino_core/application/trait.Application.html#tymethod.register
83 | [`run_with`]: https://docs.rs/zino-core/latest/zino_core/application/trait.Application.html#tymethod.run_with
84 | [`spawn`]: https://docs.rs/zino-core/latest/zino_core/application/trait.Application.html#method.spawn
85 | [`ServiceConfig`]: https://docs.rs/actix-web/latest/actix_web/web/struct.ServiceConfig.html
86 | [`Router`]: https://docs.rs/axum/latest/axum/struct.Router.html
87 | [`Routable`]: https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.Routable.html
88 |
--------------------------------------------------------------------------------
/src/basics/error.md:
--------------------------------------------------------------------------------
1 | # 错误处理
2 |
3 | 在Zino框架中,我们定义了一个通用的错误类型[`Error`],主要目的是实现以下功能:
4 |
5 | 1. 基于字符串将任意错误包装成同一类型;
6 | 2. 支持错误溯源,并能追溯到原始错误;
7 | 3. 支持传递自定义错误信息上下文类型;
8 | 4. 支持[`tracing`],自动记录错误信息。
9 |
10 | 这四条需求对Zino框架至关重要,这也是为什么我们没有采用社区中流行的错误处理库,比如[`anyhow`]。
11 | 在实际应用开发中,我们往往并不会对具体的错误类型做不同的处理[^new_error],而是直接返回错误消息,
12 | 所以我们采取基于字符串的错误处理:
13 | ```rust
14 | #[derive(Debug)]
15 | pub struct Error {
16 | message: SharedString,
17 | source: Option>,
18 | context: Option>,
19 | }
20 | ```
21 | 其中[`SharedString`]是Zino中用来优化静态字符串处理的类型[^benchmark]。
22 | 我们可以调用[`sources`]方法返回一个迭代器进行错误溯源,也可以使用[`root_source`]方法来追溯到原始错误。
23 |
24 | 对于任意满足`Send + 'static`约束的类型,我们可以将它作为错误信息上下文来传递:
25 | ```rust
26 | pub fn set_context(&mut self, context: T) {
27 | self.context = Some(Box::new(context));
28 | }
29 |
30 | pub fn get_context(&self) -> Option<&T> {
31 | self.context
32 | .as_ref()
33 | .and_then(|ctx| ctx.downcast_ref::())
34 | }
35 | ```
36 |
37 | 对于任意实现了[`std::error::Error`] trait的错误类型,我们可以将它转换为[`Error`]类型:
38 | ```rust
39 | impl From for Error {
40 | fn from(err: E) -> Self {
41 | Self {
42 | message: err.to_string().into(),
43 | source: err.source().map(|err| Box::new(Self::new(err.to_string()))),
44 | context: Some(Box::new(err)),
45 | }
46 | }
47 | }
48 | ```
49 | 这样在需要返回`Result`的函数中,我们就可以很方便地使用`?`运算符。
50 | 需要注意的是,我们的[`Error`]类型本身并没有实现[`std::error::Error`]。
51 |
52 | 通过为[`Error`]类型实现[`std::fmt::Display`],我们可以提供对[`tracing`]的集成,让它自动记录错误信息:
53 | ```rust
54 | impl std::fmt::Display for Error {
55 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 | let message = self.message();
57 | if let Some(source) = &self.source {
58 | let source = source.message();
59 | let root_source = self.root_source().map(|err| err.message());
60 | if root_source != Some(source) {
61 | tracing::error!(root_source, source, message);
62 | } else {
63 | tracing::error!(root_source, message);
64 | }
65 | } else {
66 | tracing::error!(message);
67 | }
68 | write!(f, "{message}")
69 | }
70 | }
71 | ```
72 | 每当我们调用`.to_string()`时,`tracing::error!`就会自动生成一条记录。
73 |
74 | [^new_error]: 当然,你也可以自定义可枚举的错误类型,并为其实现[`std::error::Error`] trait。
75 | [^benchmark]: 我们的[`Error`]类型对于静态字符串的处理有巨大的性能优势,具体可以参考我们的[`box_error`]测试。
76 |
77 | [`anyhow`]: https://docs.rs/anyhow
78 | [`tracing`]: https://docs.rs/tracing
79 | [`Error`]: https://docs.rs/zino-core/latest/zino_core/error/struct.Error.html
80 | [`SharedString`]: https://docs.rs/zino-core/latest/zino_core/type.SharedString.html
81 | [`sources`]: https://docs.rs/zino-core/latest/zino_core/error/struct.Error.html#method.sources
82 | [`root_source`]: https://docs.rs/zino-core/latest/zino_core/error/struct.Error.html#method.root_source
83 | [`box_error`]: https://github.com/zino-rs/zino/blob/main/zino-core/benches/box_error.rs
84 | [`std::error::Error`]: https://doc.rust-lang.org/nightly/std/error/trait.Error.html
85 | [`std::fmt::Display`]: https://doc.rust-lang.org/nightly/std/fmt/trait.Display.html
--------------------------------------------------------------------------------
/src/basics/model.md:
--------------------------------------------------------------------------------
1 | # 领域模型(TODO)
2 |
--------------------------------------------------------------------------------
/src/basics/request.md:
--------------------------------------------------------------------------------
1 | # 请求上下文
2 |
3 | 与[`actix-web`]、[`axum`]、[`ntex`]等框架不同,我们并不推荐使用提取器(Extractor)模式[^extractor],
4 | 而是采取类似于[`Express`]和[`Fiber`]那种把很多方法都挂在请求上下文的做法,这会带来几个好处:
5 |
6 | 1. 保持`controller`中只有`Request`一个参数,便于批量实现接口以及代码自动生成;
7 | 2. 允许使用者动态判断是否需要从请求中获取信息,避免使用不必要的`Option`类型;
8 | 3. 能够提供非异步的实现,比如从URI中提取查询参数这种本来就应该是同步的[^axum-query]。
9 |
10 | 当然,由于我们提供了对[`actix-web`]、[`axum`]、[`ntex`]等框架的集成,它们所支持的Handler都是可以直接在Zino中使用的,
11 | 也包括提取器模式。
12 |
13 | 我们通过[`RequestContext`]这一trait来提供请求上下文方法,它包含以下必须要实现的方法:
14 |
15 | ```rust
16 | pub trait RequestContext {
17 | type Method: AsRef;
18 | type Headers;
19 |
20 | fn request_method(&self) -> &Self::Method;
21 |
22 | fn original_uri(&self) -> &Uri;
23 | fn matched_route(&self) -> Cow<'_, str>;
24 |
25 | fn header_map(&self) -> &Self::Headers;
26 | fn get_header(&self, name: &str) -> Option<&str>;
27 | fn client_ip(&self) -> Option;
28 |
29 | fn get_context(&self) -> Option;
30 |
31 | fn get_data(&self) -> Option;
32 | fn set_data(
33 | &mut self,
34 | value: T
35 | ) -> Option;
36 |
37 | async fn read_body_bytes(&mut self) -> Result, Error>;
38 | }
39 | ```
40 | 值得注意的是,我们提供的`read_body_bytes`方法不会消费`Request`对象[^axum-body],但是它的重复调用并不保证会给出相同的结果,
41 | 具体处理方式由实现决定。
42 |
43 | [^extractor]: 有些人总觉得Rust中的提取器很神奇,其实无外乎就是泛型加上宏批量实现罢了,Rust本身并不支持可变参数函数。
44 | [^axum-query]: `axum`的`Query`提取器尽管提供了同步的[`try_from_uri`]方法,但它实现的[`from_request_parts`]却是异步的。
45 | [^axum-body]: 在`axum`中,实现了[`FromRequest`]的提取器总会消费`Request`对象,所以它们只能使用一次,并且只能作为`Handler`的最后一个参数。
46 |
47 | [`actix-web`]: https://crates.io/crates/actix-web
48 | [`axum`]: https://crates.io/crates/axum
49 | [`ntex`]: https://crates.io/crates/ntex
50 | [`Express`]: https://expressjs.com/en/5x/api.html#req
51 | [`Fiber`]: https://docs.gofiber.io/api/ctx
52 | [`RequestContext`]: https://docs.rs/zino-core/latest/zino_core/request/trait.RequestContext.html
53 | [`try_from_uri`]: https://docs.rs/axum/latest/axum/extract/struct.Query.html#method.try_from_uri
54 | [`from_request_parts`]: https://docs.rs/axum/latest/axum/extract/trait.FromRequestParts.html#tymethod.from_request_parts
55 | [`FromRequest`]: https://docs.rs/axum/latest/axum/extract/trait.FromRequest.html
56 |
--------------------------------------------------------------------------------
/src/basics/response.md:
--------------------------------------------------------------------------------
1 | # 请求响应(TODO)
2 |
--------------------------------------------------------------------------------
/src/basics/schedule.md:
--------------------------------------------------------------------------------
1 | # 任务调度(TODO)
2 |
--------------------------------------------------------------------------------
/src/basics/state.md:
--------------------------------------------------------------------------------
1 | # 状态管理
2 |
3 | 在Zino框架中,应用状态是由[`State`]类型提供的:
4 | ```rust
5 | #[derive(Debug, Clone)]
6 | pub struct State {
7 | env: Env,
8 | config: Table,
9 | data: T,
10 | }
11 | ```
12 | 它包含有运行环境[`Env`]、TOML格式的配置文件[`Table`]以及自定义数据类型`T`。
13 |
14 | 不同于很多Web框架,我们的应用状态管理是基于惰性初始化的全局变量来实现的,主要考量如下:
15 |
16 | 1. 应用状态通常存在于整个运行期间,也就是说它的生命周期为`'static`;
17 | 2. 应用状态通常只需要初始化加载一次,并且在运行期间并不会被修改;
18 | 3. 尽可能避免在`controller`或`service`方法中传递应用状态参数。
19 |
20 | 回到具体实现上,最核心的几行代码就是
21 | ```rust
22 | static SHARED_STATE: LazyLock = LazyLock::new(|| {
23 | let mut state = State::default();
24 | state.load_config();
25 | state
26 | });
27 | ```
28 | 这定义了一个全局共享的静态变量,通过[`State::shared`]方法可以得到它的一个`&'static`引用,
29 | 然后就可以在整个应用中到处使用。
30 |
31 | ## 示例:如何使用Redis?
32 |
33 | ```toml
34 | # config/config.dev.toml
35 |
36 | [redis]
37 | host = "127.0.0.1"
38 | port = 6379
39 | database = "dbnum"
40 | username = "some_user"
41 | password = "hsfU4Y3aRbxVNuLpVG5T+wb9jIDdQyaUIiPgeQrP0ZRM1g"
42 | ```
43 |
44 | ```rust
45 | //! src/extension/redis.rs
46 |
47 | use parking_lot::Mutex;
48 | use redis::{Client, Connection, RedisResult};
49 | use zino_core::{state::State, LazyLock};
50 |
51 | #[derive(Debug, Clone, Copy)]
52 | pub struct Redis;
53 |
54 | impl Redis {
55 | #[inline]
56 | pub fn get_value(key: &str) -> RedisResult {
57 | REDIS_CONNECTION.lock().get(key)
58 | }
59 |
60 | #[inline]
61 | pub fn set_value(key: &str, value: &str, seconds: u64) -> RedisResult<()> {
62 | REDIS_CONNECTION.lock().set_ex(key, value, seconds)
63 | }
64 | }
65 |
66 | static REDIS_CLIENT: LazyLock = LazyLock::new(|| {
67 | let config = State::shared()
68 | .get_config("redis")
69 | .expect("the `redis` field should be a table");
70 | let database = config
71 | .get_str("database")
72 | .expect("the `database` field should be a str");
73 | let authority = State::format_authority(config, Some(6379));
74 | let url = format!("redis://{authority}/{database}");
75 | Client::open(url)
76 | .expect("fail to create a connector to the redis server")
77 | });
78 |
79 | static REDIS_CONNECTION: LazyLock> = LazyLock::new(|| {
80 | let connection = REDIS_CLIENT.get_connection()
81 | .expect("fail to establish a connection to the redis server");
82 | Mutex::new(connection)
83 | });
84 | ```
85 | 这是Zino框架中推荐的使用模式:在配置文件里编写Redis连接信息[^password],通过Lazy的全局变量初始化Client;
86 | 然后定义一个空结构体,进而封装一些自定义方法。仔细想想,这不就类似于其他语言中的“单例模式”吗?
87 |
88 | [^password]: 配置项中的密码也可以先写为明文,运行后会在终端里提醒你修改为加密后的。
89 |
90 | [`State`]: https://docs.rs/zino-core/latest/zino_core/state/struct.State.html
91 | [`Env`]: https://docs.rs/zino-core/latest/zino_core/state/enum.Env.html
92 | [`Table`]: https://docs.rs/toml/latest/toml/type.Table.html
93 | [`State::shared`]: https://docs.rs/zino-core/latest/zino_core/state/struct.State.html#method.shared
94 |
--------------------------------------------------------------------------------
/src/guide.md:
--------------------------------------------------------------------------------
1 | # 入门指南
2 |
3 | ## 快速开始
4 |
5 | 你可以从代码仓库中的示例[`actix-app`]、[`axum-app`]、[`dioxus-desktop`]或者[`ntex-app`]开始体验Zino。
6 |
7 | ```bash
8 | git clone https://github.com/zino-rs/zino.git
9 | cd zino/examples/axum-app
10 | cargo run
11 | ```
12 |
13 | 如果你能在终端中看到日志输出,那就表明`axum-app`成功运行了!需要注意的是:
14 | - 当前Zino框架运行在`Rust 1.75+`,请使用[`rustup`]安装合适的版本;
15 | - 示例`axum-app`中需要连接MySQL数据库,具体配置参见[`axum-app/config/config.dev.toml`][axum-app-config]。
16 |
17 | [`actix-app`]: https://github.com/zino-rs/zino/tree/main/examples/actix-app
18 | [`axum-app`]: https://github.com/zino-rs/zino/tree/main/examples/axum-app
19 | [`dioxus-desktop`]: https://github.com/zino-rs/zino/tree/main/examples/dioxus-desktop
20 | [`ntex-app`]: https://github.com/zino-rs/zino/tree/main/examples/ntex-app
21 | [`rustup`]: https://rust-lang.github.io/rustup/
22 | [axum-app-config]: https://github.com/zino-rs/zino/blob/main/examples/axum-app/config/config.dev.toml
--------------------------------------------------------------------------------
/src/guide/config.md:
--------------------------------------------------------------------------------
1 | # 配置文件
2 |
3 | Zino框架支持根据运行环境来加载配置,不同环境的配置项定义在`config/config.{env}.toml`文件中。
4 | 具体运行环境的选择,按照以下优先顺序来判断:
5 |
6 | 1. 应用启动时传入的`--env`参数,如`cargo run -- --env=dev`;
7 | 2. 如果命令参数不存在,将尝试读取环境变量`ZINO_APP_ENV`[^dotenv];
8 | 3. 假如环境变量也不存在,将根据`cfg!(debug_assertions)`给出默认值:取值为`true`,则运行环境为`dev`,
9 | 否则为`prod`。
10 |
11 | 开发环境`dev`和生产环境`prod`是预定义的两个取值。如有需要,你也可以自行添加其他运行环境,比如`test`环境,
12 | 对应的配置文件为`config/config.test.toml`。
13 |
14 | 当然,除了Rust社区中最常用的TOML格式,我们也支持JSON格式的配置文件,
15 | 可以通过环境变量`ZINO_APP_CONFIG_FORMAT`进行选择。默认情况下,配置文件是从本地加载的;
16 | 如果你需要从远程URL加载,那就请设置环境变量`ZINO_APP_CONFIG_URL`,
17 | 此时配置文件格式是通过请求响应的`content_type`来判断的。
18 |
19 | 鉴于Zino框架的配置项比较多,在项目开发的初始阶段我们推荐你采用默认值,这样可以省略绝大多数的配置项。
20 | 如果确有需要,在项目开发的过程中再逐渐添加。
21 |
22 | 一个最简单的配置文件示例如下:
23 | ```toml
24 | name = "DataCube"
25 | version = "1.0"
26 | ```
27 | 需要注意的是,这里的`name`和`version`是指应用的名称和版本,与`Cargo.toml`里`[package]`的`name`和`version`不是一个概念。
28 |
29 | 在这里,我们只列出全局配置项,具体功能的配置项将在后面的功能模块里给出。
30 |
31 | - `name`:应用名称
32 | ```toml
33 | name = "DataCube"
34 | ```
35 | - `version`: 应用版本
36 | ```toml
37 | version = "1.0"
38 | ```
39 | - `secret`: 应用密文,用于推导`Application`的[`secret_key`][docsrs-secret-key]。
40 | 当缺失时,会根据应用名称和版本自动生成。
41 | ```toml
42 | secret = "SecretPhrase"
43 | ```
44 |
45 | [^dotenv]: 当开启了`feature = "dotenv"`时,Zino框架也会自动加载项目目录中的`.env`文件。
46 |
47 | [docsrs-secret-key]: https://docs.rs/zino-core/latest/zino_core/application/trait.Application.html#method.secret_key
48 |
--------------------------------------------------------------------------------
/src/guide/deployment.md:
--------------------------------------------------------------------------------
1 | # 应用部署(TODO)
2 |
--------------------------------------------------------------------------------
/src/guide/development.md:
--------------------------------------------------------------------------------
1 | # 开发测试(TODO)
2 |
--------------------------------------------------------------------------------
/src/guide/new-app.md:
--------------------------------------------------------------------------------
1 | # 创建应用
2 |
3 | 我们使用Rust的构建工具`Cargo`来管理应用。首先新建一个项目
4 | ```bash
5 | cargo new zino-app --bin
6 | ```
7 | 然后在`Cargo.toml`中添加以下依赖
8 | ```toml
9 | [package]
10 | name = "zino-app"
11 | version = "0.1.0"
12 | edition = "2021"
13 |
14 | [dependencies]
15 | zino = { version = "0.22", features = ["axum"] }
16 | ```
17 | 这里我们使用的是`axum`框架。如果要用`actix-web`框架,那就把`features`替换为`["actix"]`。
18 | 进而,我们在`src`目录的`main.rs`中添加以下代码:
19 | ```rust
20 | use zino::prelude::*;
21 |
22 | fn main() {
23 | zino::Cluster::boot().run()
24 | }
25 | ```
26 | 此时,我们的应用已经可以运行了:
27 | ```bash
28 | cargo run
29 | ```
30 | 打开浏览器地址`http://localhost:6080/rapidoc`,你将能够看到RapiDoc文档页面。
31 |
32 | 这是一个极简的示例,没有太多实际功能。但是如果你在项目目录中添加一个`public`目录,那么这就可以作为静态文件服务器,
33 | 并且Zino框架会自动使用`public/index.html`来渲染根路由`/`。在前后端分离的项目中,这一特性可用于部署打包后的单页面应用。
--------------------------------------------------------------------------------
/src/guide/structure.md:
--------------------------------------------------------------------------------
1 | # 目录结构
2 |
3 | > Zino开发框架的应用目录组织方式只是一种推荐,可根据实际需求进行调整。尤其是`src`目录,可以使用Rust模块自由组织,
4 | 我们并没有严格限定各个模块之间的调用关系,只要能通过编译皆可。
5 |
6 | 我们采用了类似于[Egg.js][eggjs-structure]的应用目录约定规范:
7 |
8 | ```shell
9 | zino-app
10 | ├─ Cargo.toml
11 | ├─ config
12 | │ ├─ config.dev.toml
13 | │ ├─ config.prod.toml
14 | │ ├─ locale
15 | │ │ ├─ en-US.ftl
16 | │ │ └─ zh-CN.ftl
17 | │ ├─ opa
18 | │ │ └─ user.opa
19 | │ └─ openapi
20 | │ ├─ OPENAPI.toml
21 | │ ├─ auth.toml
22 | │ └─ user.toml
23 | ├─ local
24 | │ ├─ data
25 | │ │ └─ mock
26 | │ │ ├─ logs.ndjson
27 | │ │ └─ users.csv
28 | │ └─ docs
29 | │ └─ rapidoc.html
30 | ├─ logs
31 | ├─ public
32 | │ ├─ 404.html
33 | │ ├─ data
34 | │ │ └─ logs.ndjson
35 | │ └─ index.html
36 | ├─ src
37 | │ ├─ controller
38 | │ │ ├─ mod.rs
39 | │ │ ├─ stats.rs
40 | │ │ ├─ task.rs
41 | │ │ └─ user.rs
42 | │ ├─ extension
43 | │ │ ├─ casbin.rs
44 | │ │ ├─ header.rs
45 | │ │ └─ mod.rs
46 | │ ├─ logic
47 | │ │ ├─ mod.rs
48 | │ │ ├─ task.rs
49 | │ │ └─ user.rs
50 | │ ├─ main.rs
51 | │ ├─ middleware
52 | │ │ ├─ access.rs
53 | │ │ └─ mod.rs
54 | │ ├─ router
55 | │ │ └─ mod.rs
56 | │ ├─ schedule
57 | │ │ ├─ job.rs
58 | │ │ └─ mod.rs
59 | │ └─ service
60 | │ ├─ mod.rs
61 | │ ├─ task.rs
62 | │ └─ user.rs
63 | └─ templates
64 | ├─ layout.html
65 | └─ output.html
66 | ```
67 |
68 | * `Cargo.toml`为应用的Cargo配置文件。
69 | * `config/config.{env}.toml`用于编写不同运行环境的配置文件。
70 | * `config/locale/{lang-id}.ftl`于编写i18n多语言文件,目前仅支持[`Fluent`]规范。
71 | * `config/opa/{policy}.rego`用于编写OpenPolicyAgent策略。
72 | * `config/openapi/{tag}.toml`用于编写OpenAPI规范文档。
73 | * `local/`为本地静态资源目录,`data/`为本地数据目录,`docs/`为文档目录。
74 | * `logs/`用于日志文件输出。
75 | * `public/`为通过网络访问的静态资源目录,`index.html`为默认首页文件,`404.html`为404文件,`data/`为共享的数据目录。
76 | * `src/controller/`用于编写控制器。
77 | * `src/extension/`用于编写辅助函数。
78 | * `src/logic/`用于编写业务逻辑。
79 | * `src/main.rs`用于启动应用以及自定义初始化。
80 | * `src/middleware/`用于编写中间件。
81 | * `src/router/`用于配置URL路由规则。
82 | * `src/schedule/`用于编写定时任务。
83 | * `src/service/`用于编写业务接口服务,供`controller`调用。
84 | * `templates/`用于编写HTML模板文件,目前支持[`Tera`]和[`MiniJinja`]模板。
85 |
86 | [eggjs-structure]: https://www.eggjs.org/zh-CN/basics/structure
87 | [`Fluent`]: https://projectfluent.org/
88 | [`Tera`]: https://docs.rs/tera
89 | [`MiniJinja`]: https://docs.rs/minijinja
90 |
--------------------------------------------------------------------------------
/src/integrations.md:
--------------------------------------------------------------------------------
1 | # 框架集成(TODO)
2 |
--------------------------------------------------------------------------------
/src/integrations/actix-web.md:
--------------------------------------------------------------------------------
1 | # actix-web(TODO)
2 |
--------------------------------------------------------------------------------
/src/integrations/axum.md:
--------------------------------------------------------------------------------
1 | # axum(TODO)
2 |
--------------------------------------------------------------------------------
/src/integrations/dioxus.md:
--------------------------------------------------------------------------------
1 | # dioxus(TODO)
2 |
--------------------------------------------------------------------------------
/src/introduction.md:
--------------------------------------------------------------------------------
1 | # 引言
2 |
3 | [`Zino`][zino]致力于打造基于[Rust][rust]语言的新一代组装式应用开发框架,
4 | 提供一站式跨平台多端解决方案,可用于后端API开发、桌面应用开发等。
5 | 我们奉行『约定优于配置』的原则,提供开箱即用的功能模块,极大提升开发效率;
6 | 并通过应用接口抽象与[`actix-web`][actix-web]、[`axum`][axum]、[`dioxus`][dioxus]、[`ntex`][ntex]等框架集成,
7 | 打通社区生态。
8 |
9 | 
10 |
11 | [rust]: https://www.rust-lang.org/
12 | [zino]: https://github.com/photino/zino
13 | [actix-web]: https://crates.io/crates/actix-web
14 | [axum]: https://crates.io/crates/axum
15 | [ntex]: https://crates.io/crates/ntex
16 | [dioxus]: https://crates.io/crates/dioxus
17 |
--------------------------------------------------------------------------------
/src/orm.md:
--------------------------------------------------------------------------------
1 | # 数据库ORM(TODO)
2 |
--------------------------------------------------------------------------------
/src/orm/default-controller.md:
--------------------------------------------------------------------------------
1 | # API自动生成(TODO)
2 |
--------------------------------------------------------------------------------
/src/orm/openapi.md:
--------------------------------------------------------------------------------
1 | # OpenAPI文档(TODO)
2 |
--------------------------------------------------------------------------------
/src/orm/table-mapping.md:
--------------------------------------------------------------------------------
1 | # 数据表映射(TODO)
2 |
--------------------------------------------------------------------------------
/src/orm/trait-derive.md:
--------------------------------------------------------------------------------
1 | # Trait推导(TODO)
2 |
--------------------------------------------------------------------------------
/src/resources.md:
--------------------------------------------------------------------------------
1 | # 更多资源
2 |
3 | ## 微信公众号
4 |
5 | 
6 |
7 | ## 知乎专栏
8 |
9 | [Zino开发框架技术解读](https://www.zhihu.com/column/c_1629805160351281152)
10 |
--------------------------------------------------------------------------------
/src/topics.md:
--------------------------------------------------------------------------------
1 | # 进阶专题(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/accessor.md:
--------------------------------------------------------------------------------
1 | # 存储服务(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/authentication.md:
--------------------------------------------------------------------------------
1 | # 身份认证(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/cache.md:
--------------------------------------------------------------------------------
1 | # 缓存(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/channel.md:
--------------------------------------------------------------------------------
1 | # 消息订阅(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/chatbot.md:
--------------------------------------------------------------------------------
1 | # 聊天机器人(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/connector.md:
--------------------------------------------------------------------------------
1 | # 数据源访问(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/datetime.md:
--------------------------------------------------------------------------------
1 | # 日期处理(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/extension.md:
--------------------------------------------------------------------------------
1 | # 辅助函数(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/i18n.md:
--------------------------------------------------------------------------------
1 | # 国际化(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/observability.md:
--------------------------------------------------------------------------------
1 | # 可观测性(TODO)
2 |
--------------------------------------------------------------------------------
/src/topics/view.md:
--------------------------------------------------------------------------------
1 | # HTML模板(TODO)
2 |
--------------------------------------------------------------------------------
/theme/style.css:
--------------------------------------------------------------------------------
1 | @media only screen and (max-width:1080px) {
2 | .sidetoc {
3 | display: none !important;
4 | }
5 | }
6 |
7 | table {
8 | margin-left: 0 !important;
9 | }
10 |
11 | @media only screen and (max-width: 2560px) {
12 | .nav-wide-wrapper {
13 | display: none;
14 | }
15 |
16 | .nav-wrapper {
17 | display: block;
18 | }
19 | }
20 |
21 | @media only screen and (max-width: 2560px) {
22 | .sidebar-visible .nav-wide-wrapper {
23 | display: none;
24 | }
25 |
26 | .sidebar-visible .nav-wrapper {
27 | display: block;
28 | }
29 | }
30 |
31 | code {
32 | word-break: break-word;
33 | }
34 |
35 | code.editable,
36 | .ace_scroller {
37 | top: 10px;
38 | }
39 |
40 | .content p {
41 | text-align: justify;
42 | }
--------------------------------------------------------------------------------