普通高等学校健康驿站建设指引做问卷的网站生成二维码
普通高等学校健康驿站建设指引,做问卷的网站生成二维码,网站建设成本包括什么,wordpress loren今天我们来学习 Phoenix 的视图和组件#xff0c;我们通过它们来渲染前端页面。其实严格来说#xff0c;Phoenix 中并没有视图这个东西#xff0c;视图只不过是一个组件(Component)。没错#xff0c;组件既是视图#xff0c;也是用来构建视图的积木#xff0c;这也是”组…今天我们来学习 Phoenix 的视图和组件我们通过它们来渲染前端页面。其实严格来说Phoenix 中并没有视图这个东西视图只不过是一个组件(Component)。没错组件既是视图也是用来构建视图的积木这也是”组件”这个名称的由来。这里我们将要介绍的是 Phoenix 中的静态视图因为 Phoenix 中还有动态视图我们在《Phoenix 动态视图与动态组件》中再介绍它。虽然 Phoenix 中并没有视图但是确实有动态视图。接下来我们将不再区分视图和组件这两个概念因为它们是同一个东西。但为了描述方便会混用这两个名词希望不会引起误解。组件的本质组件本质上只是一个返回 HEEx 模板的 Elixir 函数函数接受一个名叫assigns的参数参数中是要填入模板中的数据。虽然函数参数名称是可以任意的但是在这里参数名叫做assigns是一种人文约定Phoenix 文档中也是直接用 assigns 来指代这个参数。def index(assigns) do ~H H1Hello Phoenix!/H1 endHEEx HTML EEx(EmbeddedElixir)。也就是嵌入了 Elixir 表达式的 HTMLEEx 是 Elixir 的表达式求值引擎。渲染视图视图的渲染通常在 Controller 中完成。以 Phoenix 模板工程为例在PageController中我们通过Controller模块提供的render函数来渲染首页视图。defmodule HelloWeb.PageController do use HelloWeb, :controller def home(conn, _params) do # The home page is often custom made, # so skip the default app layout. render(conn, :home, layout: false) end end这里:home就是我们要渲染的视图一个名为home的函数。第一个问题视图究竟在哪儿呢Phoenix 会根据 Controller 的名称和视图类型来决定去哪里寻找视图。控制器模块通常会命名为SomeControllerSome是控制器名称那么 Phoenix 就会去SomeTYPE模块中寻找视图函数TYPE是视图类型比如HTML表示 html 相应JSON就表示 json 相应。这是 Phoenix 的命名约定我们应该遵守它。因此上例中 Phoenix 会去PageHTML模块中去寻找home函数。第二个问题视图类型怎么确定眼下还有一个问题视图类型究竟是什么。不过在此之前我们可以先通过get_format/1函数看看响应类型是什么他也是Phoenix.Controller模块的函数因此可以在控制器中直接调用。def home(conn, _params) do # The home page is often custom made, # so skip the default app layout. IO.puts(format: #{get_format(conn)}) render(conn, :home, layout: false) end刷新网页我们会看到下面的日志。format: html不错确实是 html。接下来我们来看看为什么以及怎么去设置类型。在router.ex中找到PageController路由。scope /, HelloWeb do pipe_through :browser get /, PageController, :home end在这个路由中我们使用了:browser管线而它的定义如下。pipeline :browser do plug :accepts, [html] ... ... end这里我们使用了acceptsplug它的作用就是与前端协商响应格式这里我们接受 html 格式。打开浏览器调试查看请求的 Accept Header。在请求的 Accept 头里面也支持 html 格式于是就这么愉快的决定了。如果我们注释掉plug :accepts, [html]再次刷新网页就会看到如下的错误。这说明格式其实是存储在请求参数的_format字段中的。另一种直接由前端决定请求格式的方式是在请求参数中加上_format参数http://localhost:4000/?_formathtml。当请求中没有_format参数时才会去解析 Accept 头。浏览器由于历史原因 Accept 头比较混乱Phoenix 的策略是只要有以下情况之一就设置成 htmlAccept 列表中包含 html 格式指定了多种类型且包含*/*通配格式。acceptsplug 除了用来协商格式也指定了一个支持的格式列表对于格式不在支持列表内的请求Phoenix 会拒绝响应。除了由客户端指定响应格式服务端也可以使用put_format直接设置格式并且具有最高优先级。pipeline :browser do # plug :accepts, [html] plug :put_format, html ... ... end在服务端设置响应格式的另一种方式是在控制器的render函数中使用字符串参数view.type指定视图参数view是视图函数名称type是响应类型。def home(conn, _params) do # The home page is often custom made, # so skip the default app layout. IO.puts(format: #{get_format(conn)}) render(conn, home.html, layout: false) end这里我们用home.html来指定视图表示渲染视图来自 html 模板中的home函数它并不表示home.html文件事实上也并不存在这个文件。这种根据 format 返回不同格式响应的能力让我们可以在同一个 uri 上提供多种响应格式如 html 和 json而不必为此开两个路由。如果想要在accetpsplug 中使用自定义类型需要在config/config.exs中进行配置。config :mime, :types, %{ application/vnd.apijson [json-api] }这个配置是一个 map键是 media type值是在 phoenix 中使用的类型。配置完以后还需要重新编译 plug。mix deps.clean mime --build mix deps.get然后就可以使用json-api这个格式了。plug :accepts, [html, json-api]现在我们知道去哪里寻找视图函数了打开page.html.ex文件并没有一个名为home的函数存在。但是有embed_templates page_html/*这么一行代码。这是 phoenix 框架为我提供的一个便利embed_templates可以将.heex模板文件嵌入模块中编译成一个视图函数支持通配或者指定具体文件。文件名命名为some.type.heexsome既是文件名也是最后编译出的视图函数名。当然只有type匹配当前模块格式的文件才会被嵌入。例如在示例的page_html目录下只有一个home.html.heex文件它最终会嵌入PageHTML模块变成它里面的一个名为home的函数。虽然.heex模板放在哪个目录下并无要求但是按照 phoenix 的命名规约一般都会将.heex模板文件放到和视图模块文件同名的目录下。.heex文件中的内容就是heex模板也就是视图函数中~H和之间的内容。当模板内容比较长的时候用单独的文件来管理这些模板是更加明智的选择它可以防止视图模块变得冗长和难以阅读。如果我们遵守 phoenix 的规则它会自动帮我们确定视图模块当然我们也可以使用put_view/2函数手动设置视图模块。让我们在controllers目录下创建一个page_view.ex文件并输入以下内容defmodule HelloWeb.PageView do use HelloWeb, :html def home(assigns) do ~H h1Hello Phoenix!/h1 end end然后回到page_controller.ex中修改home函数如下def home(conn, _params) do # The home page is often custom made, # so skip the default app layout. IO.puts(format: #{get_format(conn)}) conn | put_view(HelloWeb.PageView) | render(:home, layout: false) end刷新页面现在我们将看到Hello Phoenix!显示在页面中。put_view/2还支持为多个格式设置不同的视图模块比如put_view(conn, html: AppHTML, json: AppJSON)HEEx语法最简单的 HEEx 模板就是单纯的 html 片段。h1Hello Phoenix!/h1当然我们并不满足于如此简单的用法我还希望可以根据参数来生成 html 片段。def home(assigns) do ~H h1Hello % assigns.name %/h1 end因为所有变量都是通过assigns来访问的因此 phoenix 允许我们通过来访问变量减少键盘敲击。h1Hello % name %/h1% %会求值其中的 Elixir 表达式并将结果插入此处而% %只会求值其中的表达式但不会插入结果。如果是在标签内部插值则要使用{xxx}比如设置标签属性。h1 class{class}.../h1对于h1 class{class}我们可以传递一个列表指定多个属性。conn | assign(:class, [text-2xl, font-medium]) | render(:home)它会渲染为h1 classtext-2xl font-medium。而对于h1 {myattr}则可以传递一个关键字列表或 map 来设置属性。conn | assign(:myattr, [class: btn, id: mybtn]) | render(:home)对于编写 html 模板使用if和for语句来控制内容显示以及循环生成内容是很常见的需求HEEx 当然也支持。if语法有两种格式% if expr do % ... ... % end % p :if{expr}.../p举个例子% if show do % h1 classtext-2xl font-mediumHello % name %/h1 % end % p :if{show}phoenix is so good!/p我们可以通过show变量来控制内容的显示conn | assign(:show, true) | assign(:name, phoenix) | render(:home)for自然也有两种语法格式% for item - items do % ... ... % end % li :for{item - items}.../li举个例子ul % for name - names do % li% name %/li % end % /ul hr / ol li :for{name - names}% name %/li /ol我们这样去渲染它conn | assign(:names, [elixir, go, haskell]) | render(:home)组件是构建视图的积木就像函数可以调用其他函数一样HEEx 模板内也可以调用其他组件以此来复用组件构建视图。调用函数组件只需要把 html 标签名换成函数名就可以了。defmodule HelloWeb.PageView do use HelloWeb, :html def home(assigns) do ~H HelloWeb.PageView.hello / end def hello(assigns) do ~H h1Hello Phoenix!!/h1 end end如果两个函数在同一个模块内可以省略模块名直接写成.hello /其他模块的函数组件需要通过模块名访问当然如果我们提前通过import导入模块的话也可以使用.函数名的简写形式。defmodule HelloWeb.PageView do use HelloWeb, :html import HelloWeb.PageHTML def home(assigns) do ~H .hello / .hello_phoenix / HelloWeb.PageHTML.hello_phoenix / end def hello(assigns) do ~H h1Hello Phoenix!!/h1 end end既然可以调用函数那么要是能传递参数就更好了。参数可以通过 html 标签属性的方式传递html 标签属性是一个键值对列表刚好符合assigns参数的格式。举个例子def home(assigns) do ~H .hello nameTom age20 / end def hello(assigns) do ~H h1Im % name %, % age % years old./h1 endHtml 语法要求属性的值必须是字符串如果传递给hello组件的参数来自assigns或者是 Elixir 表达式可以使用{}例如.hello name{name} age{age} / .hello name{Tom} age{20} /组件统一都接受assigns参数看不出来组件究竟需要哪些参数这对于函数调用者来说一点儿也不友好。我们可以使用attr/3宏来声明组件需要接受哪些参数它的作用范围是其后的所有组件直到下一个attr/3宏出现。除了提高可读性编译器也可以据此提供一些有用的信息。例如attr :name, :string, required: true attr :age, :integer, required: true def hello(assigns) do ~H h1Im % name %, % age % years old./h1 endattr/3有三个参数attr(name, type, opts \\ [])。name原子类型的属性名对于同一个组件属性名不能同名也不能和槽名相同(什么是槽我们稍后介绍)。type原子类型的属性类型。opts一个关键字列表选项默认为[]。属性支持以下类型:any任意类型:string字符串类型:atom原子(包括true,false和nil):boolean布尔类型:integer整数类型:float浮点数类型:list列表类型:mapmap 类型:global任何通用 HTML 属性加上:global_prefixes定义的属性struct module使用defstruct/1宏定义了结构体的模块:global是一个比较特殊的类型它会把标签中剩余的通用 html 属性全部提取出来变成一个 map 传递给这个字段。举个例子def home(assigns) do ~H .notification messagethis is message. classbg-green-200 idmessage / end attr :message, :string, required: true attr :rest, :global def notification(assigns) do IO.inspect(assigns.rest) ~H span {rest}% message %/span end渲染结果如下spanclassbg-green-200idmessagethis is message./span控制台还会打印出以下内容%{class: bg-green-200, id: message}因为:global类型的属性是 map 类型的因此当给它设置默认值的时候也要使用 map。默认值会合并到最终的结果中。attr :rest, :global, default: %{class: bg-blue-200}除了 html 通用属性许多 js 库也会扩展 html 属性来实现特殊的功能。如果要将这些通用属性之外的属性也合并到:global字段中我们就需要告诉attr/3宏把这些属性也涵盖进来有两种方式。第一种方式是直接在attr/3中使用include选项attr :rest, :global, include: ~w(my-data)这里我们增加了my-data属性。它是一个列表适合临时添加单独修改。第二种是在use Phoenix.Component时通过global_prefixes选项来添加自定义属性前缀。phoenix 将库的导入都收归到了lib/项目_web.ex文件中这里我们修改hello_web.ex中的html函数即可。def html do quote do use Phoenix.Component, global_prefixes: ~w(my-) ... .. end end这种方式只要匹配前缀就行适合大规模统一设置默认的前缀有phx-这是 phoenix 实现动态视图的属性在动态视图中我们再介绍。最后要特别注意的就是:global属性的值是 phoenix 自动传递的我们不可以显示手动传递。下面这种写法是不对的。.notification messagethis is message. rest{%{class bg-green-200}} /我们继续来看attr/3宏的第三个参数看看它支持的选项。:required- 标记属性为必传如果调用者没有传递该属性编译器会给出警告。:default- 属性默认值它会合并到assigns中。:exapmles- 一个属性值的示例列表用于生成文档。:values- 一个属性可接受值的列表如果调用者传递的值不在该列表内编译器会给出警告它适用于枚举类型的属性。:doc- 属性文档描述。仅仅是传递参数还不够我们还希望可以动态生成标签内的内容。比如我们有一个div元素块里面既可以显示文本也可以显示图片。换句话说我们希望组件可以接受一个 html 片段就像函数可以接受另一个函数做为参数。html 片段也是通过assigns参数传递的调用函数组件本身也是一个 html 标签标签内的 html 片段通过assigns.inner_block访问我们可以使用render_slot/1函数在组件内渲染 html 片段。phoenix 将它称为插槽(slot)举个例子def home(assigns) do ~H .blue_div pHello, Phoenix!/p /.blue_div end def blue_div(assigns) do ~H div classbg-blue-200 % render_slot(inner_block) % /div end示例中的inner_block对应着pHello, Phoenix!/p它就像是一个匿名函数组件叫做匿名插槽。如果我们用IO.inspect(assigns.inner_block)打印出inner_block可以看到如下的输出[ %{ __slot__: :inner_block, inner_block: #Function5.83536033/2 in HelloWeb.PageView.home/1 } ]既然有匿名那就可以命名命名插槽语法为:slot_name.../:slot_name命名插槽也通过render_slot函数渲染。举个例子def home(assigns) do ~H .blue_div pHello, Phoenix!/p :headerh1title/h1/:header /.blue_div end def blue_div(assigns) do ~H div classbg-blue-200 % render_slot(header) % % render_slot(inner_block) % /div end注意到前面我们用IO.inspect(assigns.inner_block)打印inner_block时结果是一个列表这就意味着插槽是可以同名的同名插槽构成一个列表。插槽本身也是一段 html 模板自然也可以接受参数。就像函数参数分为形参和实参一样定义函数的参数叫形参调用函数的参数叫实参。插槽也一样渲染插槽的参数叫实参定义插槽的参数叫形参形参通过:let属性来声明。举个例子def home(assigns) do ~H .unordered_list :let{fruit} entries{~w(apples bananas cherries)} I like b% fruit %/b ! /.unordered_list end def unordered_list(assigns) do ~H ul % for entry - entries do % li% render_slot(inner_block, entry) %/li % end % /ul end它渲染的结果如下ul liI like bapples/b !/li liI like bbananas/b !/li liI like bcherries/b !/li /ul注意到我们在I like b% fruit %/b !中访问fruit时并没有使用这也就是说fruit并不是来自于assigns参数而是来自:let{fruit}中的fruit。下面这张图可以清晰的看到参数的传递过程。我们有attr/3宏用来声明组件需要哪些参数相应的我们有slot宏来声明组件需要哪些插槽。slot宏有两个分支slot/2和slot/3。slot(name, opts \\ []) slot(name, opts, block)name- 原子类型的插槽名对于同一个组件插槽不能同名也不能和attr属性同名。opts- 关键字列表选项默认为[]。block- 包含attr/3的代码块默认为nil。插槽本身也是一个 HEEx 模板类似一个匿名组件也可以接受参数自然也可以通过attr/3声明插槽可以接受哪些参数。举个例子slot :inner_block, required: true slot :header, required: true do attr :title, :string, required: true end然而在slot中定义attr还是有一些限制的。首先是不能有默认值其次只有命名插槽可以有attr代码块也就是说:inner_block是不可以定义attr代码块的。插槽选项只有两个required- 标记一个插槽是必须的如果未提供编译器会给出警告。:doc- 插槽文档。最后让我们来看一个稍微复杂点的例子def home(assigns) do ~H .simple_table rows{[%{name: Jane, age: 34}, %{name: Bob, age: 51}]} :column :let{user} labelName % user.name % /:column :column :let{user} labelAge % user.age % /:column /.simple_table end slot :column, doc: Columns with column labels do attr :label, :string, required: true, doc: Column label end attr :rows, :list, default: [] def simple_table(assigns) do ~H table tr % for col - column do % th% col.label %/th % end % % for row - rows do % tr % for col - column do % td% render_slot(col, row) %/td % end % /tr % end % /tr /table end它会渲染这样一个表格tabletrthName/ththAge/th/trtrtdJane/tdtd34/td/trtrtdBob/tdtd51/td/tr/table在.simple_table标签内我们定义了两个同名插槽:column前面我们就说过插槽是一个列表。而在simple_table函数中我们通过for循环遍历插槽来渲染表格的每一列。在lib/项目_web/components/core_components.ex文件中有很多 phoenix 预先为我们提供的函数组件同时也是非常好的学习示例。最后我们再来总结一些 HEEx 中的那些符号。符号说明示例访问assigns中的数据相当于assigns.name% %求值 Elixir 表达式并插入模板中% name %% %求值 Elixir 表达式但不插入模板中% end %{}在标签内插值p class{class}.../p.调用函数组件MyView.hello /的简写形式.hello /:定义插槽:header.../:header:let定义插槽形参p :let{name}% name %/pif条件表达式% if admin do % ... % end %或p :if{admin}.../pfor循环表达式% for item - items do %...% end %或li :for{item - items}.../li