GN语法和操作
GN语法和操作
发布时间: 1:22:01
编辑:www.fx114.net
本篇文章主要介绍了"GN语法和操作",主要涉及到GN语法和操作方面的内容,对于GN语法和操作感兴趣的同学可以参考一下。
GN语法和操作
本文描述了许多GN的语法细节和行为。
1.1使用内置的帮助!
GN具有广泛的内置帮助系统,为每个函数和内置变量提供参考。 这个页面更高级。
您还可以阅读2016年3月的GN介绍的幻灯片。演讲者备注包含完整内容。
1.2设计理念
编写构建文件不应该有太多的创造性的工作。理想情况下,给定相同的要求,两个人应该生成相同的构建文件。
除非确实需要,否则不应有灵活性。因为许多事情应该尽可能作为致命错误。
定义应该更像代码而不是规则。我不想写或调试Prolog。但是我们团队中的每个人都可以编写和调试C ++和Python。
应该考虑构建语言应该如何工作。它没必要去容易甚至是可能表达抽象事物。
我们应该通过改变源码或工具来使构建更加简单,而不是使构建加以复杂来满足外部需求(在合理的范围内)。
像Blaze,它什么时候是有意义的(见下文“与Blaze的区别和相似之处”)。
GN使用了一个非常简单的动态类型语言。的类型是:
* 布尔(true,false)。
* 64位有符号整数。
* 字符串。
* 列表(任何其它类型)。
* 作用域(有点像字典,只是内置的东西)。
有一些内置的变量,其值取决于当前的环境。参见gn help更多。
GN中有许多故意的遗漏。例如没有用户定义的函数调用(模板是最接近的了)。 根据上述设计理念,如果你需要这种东西,已经不符合其设计理念了。
变量sources有一个特殊的规则:当分配给它时,排除模式的列表会应用于它。 这是为了自动过滤掉一些类型的文件。 有关更多信息,请参阅gn help set_sources_assignment_filter和gn help label_pattern。
书呆子似的GN的完整语法在gn help grammar中提供。
字符串用双引号和反斜线使用作为转义字符。唯一支持的转义序列是:
* \” (字符引用)
* $ (字符美元符号)
* \ (字符反斜杠)
反斜杠的任何其他用法被视为字符反斜杠。 因此,例如在模式中使用的\b不需要转义,Windows路径,如“C\foo\bar.h”也不需要转义。
通过支持简单的变量替换,其中美元符号后面的字替换为变量的值。如果没有非variable-name字符以终止变量名,可以使用选择性地包围名称。不支持更复杂的表达式,只支持变量名称替换。a=“mypath”b=“a/foo.cc” # b -& “mypath/foo.cc”
c = “foo{a}bar.cc”&&c&-&&“foomypathbar.cc”&&你可以使用“0xFF”语法编码8位字符,所以带有换行符(十六进制0A)的字符串如下:`“look0x0Alike0x0Athis”。
我们是没有办法得到列表的长度的。 如果你发现自己想做这样的事情,那是你在构建中做了太多的工作。
列表支持附加:
a = [ "first" ]
a += [ "second" ]
# [ "first", "second" ]
a += [ "third", "fourth" ]
# [ "first", "second", "third", "fourth" ]
b = a + [ "fifth" ]
# [ "first", "second", "third", "fourth", "fifth" ]
将列表附加到另一个列表将项目附加在第二个列表中,而不是将列表附加为嵌套成员。
您可以从列表中删除项目:
a = [ "first", "second", "third", "first" ]
b = a - [ "first" ]
# [ "second", "third" ]
a -= [ "second" ]
# [ "first", "third", "fourth" ]
列表中的 - 运算符搜索匹配项并删除所有匹配的项。 从另一个列表中减去列表将删除第二个列表中的每个项目。
如果没有找到匹配的项目,将抛出一个错误,因此您需要提前知道该项目在那里,然后再删除它。 假定没有办法测试是否包含,主要用例是设置文件或标志的主列表,并根据各种条件删除不适用于当前构建的主列表。
在文体风格上,只喜欢添加到列表,并让每个源文件或依赖项出现一次。 这与Chrome团队为GYP提供的建议(GYP更愿意列出所有文件,然后删除在条件语中不需要的文件)相反。
列表支持基于零的下标来提取值:
a = [ "first", "second", "third" ]
# -& "second"
[]运算符是只读的,不能用于改变列表。 主要用例是当外部脚本返回几个已知值,并且要提取它们时。
在某些情况下,当你想要附加到列表时,很容易覆盖列表。 为了帮助捕获这种情况,将非空列表赋给包含非空列表的变量是一个错误。如果要解决此限制,请先将空列表赋给目标变量。如下:
a = [ “one” ]
a = [ “two” ] # Error: overwriting nonempty list with a nonempty list.
a = [] # OK
a = [ “two” ] # OK
注意,构建脚本执行时并不理解潜在数据的含义。这意味着它并不知道sources是一个文件名的列表。例如,如果你想删除其中一项,它必须字符串完全匹配,而不是指定一个不带后缀的名称,它并不会解析为相同的文件名。
2.3条件语句
条件表达式看起来像C:
if (is_linux || (is_win && target_cpu == "x86")) {
sources -= [ "something.cc" ]
} else if (...) {
如果目标只应在某些情况下声明,你就可以使用条件语句,甚至是整个目标。
你可以用foreach遍历一个列表。 但不鼓励这样做。构建做的大多数事情应该都可以表达,而不是循环,如果你发现有必要使用循环,这可能说明你在构建中做了一些无用的工作。
foreach(i, mylist) {
# Note: i is a copy of each element, not a reference to it.
2.5函数调用
简单的函数调用看起来像大多数其他的语言:
print(“hello, world”)
assert(is_win, “This should only be executed on Windows”)
这些功能是内置的,用户不能定义新的功能。
一些函数接受一个由{}括起来的代码块:
static_library("mylibrary") {
sources = [ "a.cc" ]
其中大多数定义目标。 用户可以使用下面讨论的模板机制来定义这样的functions。
确切地说,这个表达式意味着该块变成用于函数执行的函数的参数。 大多数块风格函数执行块并将结果作为要读取的变量的字典。
2.6作用域和执行
文件和函数调用后跟{}块将引入新作用域。 作用域是嵌套的。 当你读取变量时,将以相反的顺序搜索包含的作用域,直到找到匹配的名称。 变量写入始终位于最内层。
没有办法修改除最内层之外的任何封闭作用域。 这意味着,当你定义一个目标,例如,你在块里面做的什么都不会“泄漏”到文件的其余部分。
if/else/foreach语句,即使他们使用{},不会引入一个新的作用域,所以更改会影响到语句之外。
3.1文件和目录名
文件和目录名称是字符串,并且被解释为相对于当前构建文件的目录。 有三种可能的形式:
相对名称:
“foo.cc”
“SRC/foo.cc”
“../src/foo.cc”
Source-tree绝对名称:
系统绝对名称(很少,通常用于包含目录):
"/usr/local/include/"
"/C:/Program Files/Windows Kits/Include"
标识是有着预定义格式的字符串,依赖图中所有的元素(目标,配置和工具链)都由标识唯一识别。通常情况下,标识看去是以下样子。
“//base/test:test_support”
它由三部分组成:source-tree绝对路径,冒号和名称。上面这个标识指示到/base/test/BUILD.gn中查找名称是“test_support”的标识。
当加载构建文件时,如果在相对于source root给定的路径不存在时,GN将查找build/secondary中的辅助树。该树的镜像结构主存储库是一种从其它存储库添加构建文件的方式(那些我们无法简单地合入BUILD文件) 辅助树只是备用而不是覆盖,因此正常位置中的文件始终优先。
完整的标识还包括处理该标识要使用的工具链。工具链通常是以继承的方式被默认指定,当然你也可以显示指定。
“//base/test:test_support(//build/toolchain/win:msvc)”
上面这个标识会去“//build/toolchain/win”文件查到名叫”msvc”的工具链定义,那个定义会知道如何处理“test_support”这个标识。
如果你指向的标识就在此个build文件,你可以省略路径,而只是从冒号开始。
你可以以相对于当前目录的方式指定路径。标准上说,要引用非本文件中标识时,除了它要在不同上下文运行,我们建议使用绝对路径。什么是要在不同上下文运行?举个列子,一个项目它既要能构造独立版本,又可能是其它项目的子模块。
“source/plugin:myplugin” # Prefer not to do these.
“../net:url_request”
书写时,可以省略标识的第二部分、第三部分,即冒号和名称,这时名称默认使用目录的最后一段。标准上说,在这种情况建议省略第二、三部分。(以下的“=”表示等同)
“//net” = “//net:net”
“//tools/gn” = “//tools/gn:gn”
4.构建配置
4.1总体构建流程
在当前目录查找.gn文件,如果不存在则向上一级目录查找直到找到一个。将这个目录设为”souce root”, 解析该目录下的gn文件以获取build confing文件名称。
执行build config文件(这是一个默认工具链),在chromium中是//build/config/BUILDCONFIG.
加载root目录下的BUILD.gn文件;
根据root目录下的BUILD.gn内容加载其依赖的其它目录下的BUILD.gn文件,如果在指定位置找不到一个gn文件,GN将查找 build/secondary 的相应位置;
当一个目标的依赖都解决了,编译出.ninja文件保存到out_dir/dir,例如./out/arm/obj/ui/web_dialogs/web_dialogs.
当所有的目标都解决了, 编译出一个根 build.ninja 文件存放在out_dir根目录下。
4.2构建配置文件
第一个要执行的是构建配置文件,它在指示源码库根目录的“.gn”文件中指定。Chrome源码树中该文件是“//build/config/BUILDCONFIG.gn”。整个系统有且只有一个构造配置文件。
除设置其它build文件执行时的工作域外,该文件还设置参数、变量、默认值,等等。设置在该文件的值对所有build文件可见。
每个工具链会执行一次该文件(见“工具链”)。
4.3构建参数
参数可以从命令行(和其他工具链,参见下面的“工具链”)传入。 你可以通过declare_args声明接受哪些参数和指定默认值。
有关参数是如何工作的,请参阅gn help buildargs。 有关声明它们的细节,请参阅gn help declare_args。
在给定作用域内多次声明给定的参数是一个错误。 通常,参数将在导入文件中声明(在构建的某些子集之间共享它们)或在主构建配置文件中(使它们是全局的)。
4.4默认目标
您可以为给定的目标类型设置一些默认值。 这通常在构建配置文件中完成,以设置一个默认配置列表,它定义每个目标类型的构建标志和其他设置信息。
请参阅gn help set_defaults。
例如,当您声明static_library时,将应用静态库的目标默认值。 这些值可以由目标覆盖,修改或保留。
# This call is typically in the build config file (see above).
set_defaults("static_library") {
configs = [ "//build:rtti_setup", "//build:extra_warnings" ]
# This would be in your directory
static_library("mylib") {
# At this point configs is set to [ "//build:rtti_setup", "//build:extra_warnings" ]
# by default but may be modified.
configs -= "//build:extra_warnings"
configs += ":mylib_config"
# Add some more configs.
用于设置目标默认值的其他用例是,当您通过模板定义自己的目标类型并想要指定某些默认值。
目标是构造表中的一个节点,通常用于表示某个要产生的可执行或库文件。目标经常会依赖其它目标,以下是内置的目标类型(参考gn help ):
action:运行一个脚本来生成一个文件。
action_foreach:循环运行脚本依次产生文件。
bundle_data:产生要加入Mac/iOS包的数据。
create_bundle:产生Mac/iOS包。
executable:生成一个可执行文件。
group:包含一个或多个目标的虚拟节点(目标)。
shared_library:一个.dll或的.so。
loadable_module:一个只用于运行时的.dll或.so。
source_set:一个轻量的虚拟静态库(通常指向一个真实静态库)。
static_library:一个的.lib或某文件(正常情况下你会想要一个source_set代替)。
你可以用模板(templates)来扩展可使用的目标。Chrome就定义了以下类型。
component:基于构造类型,或是源文件集合或是共享库。
test:可执行测试。在移动平台,它用于创建测试原生app。
app:可执行程序或Mac/iOS应用。
android_apk:生成一个APK。有许多Android应用,参考//build/config/android/rules.gni。
6. CONFIGS
配置是指定标志集,包含目录和定义的命名对象。 它们可以应用于目标并推送到依赖目标。
config("myconfig") {
includes = [ "src/include" ]
defines = [ "ENABLE_DOOM_MELON" ]
将配置应用于目标:
executable("doom_melon") {
configs = [ ":myconfig" ]
build config文件通常指定设置默认配置列表的目标默认值。 根据需要,目标可以添加或删除到此列表。 所以在实践中,你通常使用configs + =“:myconfig”附加到默认列表。
有关如何声明和应用配置的更多信息,请参阅gn help config。
公共CONFIGS
目标可以将设置应用于依赖它的目标。 最常见的例子是第三方目标,它需要一些定义或包含目录来使其头文件正确include。 希望这些设置适用于第三方库本身,以及使用该库的所有目标。
为此,我们需要为要应用的设置编写config:
config("my_external_library_config") {
includes = "."
defines = [ "DISABLE_JANK" ]
然后将这个配置被添加到public_configs。 它将应用于该目标以及直接依赖该目标的目标。
shared_library("my_external_library") {
public_configs = [ ":my_external_library_config" ]
依赖目标可以通过将你的目标作为“公共”依赖来将该依赖树转发到另一个级别。
static_library("intermediate_library") {
public_deps = [ ":my_external_library" ]
目标可以将配置转发到所有依赖项,直到达到链接边界,将其设置为all_dependent_config。 但建议不要这样做,因为它可以喷涂标志和定义超过必要的更多的构建。 相反,使用public_deps控制哪些标志适用于哪里。
在Chrome中,更喜欢使用构建标志头系统(build/buildflag_header.gni)来防止编译器定义导致的大多数编译错误。
Toolchains 是一组构建命令来运行不同类型的输入文件和链接的任务。
可以设置有多个 Toolchains 的 build。 不过最简单的方法是每个 toolchains 分开 build同时在他们之间加上依赖关系。 这意味着,例如,32 位 Windows 建立可能取决于一个 64 位助手的 target。 他们每个可以依赖“//base:base”将 32 位基础背景下的 32 位工具链,和 64 位背景下的 64 位工具链。
当 target 指定依赖于另一个 target,当前的 toolchains 是继承的,除非它是明确覆盖(请参见上面的“Labels”)。
7.1工具链和构建配置
当你有一个简单的版本只有一个 toolchain,build config 文件是在构建之初只加载一次。它必须调用 set_default_toolchain 告诉 GN toolchain 定义的 label 标签。 此 toolchain 定义了需要用的编译器和连接器的命令。 toolchain 定义的 toolchain_args 被忽略。当 target 对使用不同的 toolchain target 的依赖, GN 将使用辅助工具链来解决目标开始构建。 GN 将加载与工具链定义中指定的参数生成配置文件。 由于工具链已经知道, 调用 set_default_toolchain 将被忽略。所以 oolchain configuration 结构是双向的。 在默认的 toolchain(即主要的构建 target)的配置从构建配置文件的工具链流向: 构建配置文件着眼于构建(操作系统类型, CPU 架构等)的状态, 并决定使用哪些 toolchain(通过 set_default_toolchin)。 在二次 toolchain,配置从 toolchain 流向构建配置文件:在 toolchain 定义 toolchain_args 指定的参数重新调用构建。
7.2工具链例子
假设默认的构建是一个 64 位版本。 无论这是根据当前系统默认的 CPU 架构, 或者用户在命令行上传递 target_cpu=“64”。 build config file 应该像这样设置默认的工具链:
# Set default toolchain only has an effect when run in the context of
# the default toolchain. Pick the right one according to the current CPU
# architecture.
if (target_cpu == "x64") {
set_default_toolchain("//toolchains:64")
} else if (target_cpu == "x86") {
set_default_toolchain("//toolchains:32")
如果一个 64 位的 target 要依靠一个 32 位二进制数, 它会使用 data_deps 指定的依赖关系(data_deps 依赖库在运行时才需要链接时不需要, 因为你不能直接链接 32 位和 64位的库)。
executable("my_program") {
if (target_cpu == "x64") {
data_deps = [ ":helper(//toolchains:32)" ]
if (target_cpu == "x86") {
shared_library("helper") {
上述(引用的工具链文件toolchains/BUILD.gn)将定义两个工具链:
toolchain("32") {
tool("cc") {
... more tools ...
toolchain_args = {
current_cpu = "x86"
toolchain("64") {
tool("cc") {
... more tools ...
toolchain_args = {
current_cpu = "x64"
工具链args明确指定CPU体系结构,因此如果目标依赖于使用该工具链的东西,那么在重新调用该生成时,将设置该cpu体系结构。 这些参数被忽略为默认工具链,因为当他们知道的时候,构建配置已经运行。 通常,工具链args和用于设置默认工具链的条件应该一致。
有关多版本设置的好处是, 你可以写你的目标条件语句来引用当前 toolchain 的状态。构建文件将根据每个 toolchain 不同的状态重新运行。 对于上面的例子 my_program, 你可以看到它查询 CPU 架构, 加入了只依赖该程序的 64 位版本。 32 位版本便不会得到这种依赖性。
7.3声明工具链
工具链均使用 toolchain 的命令声明, 它的命令用于每个编译和链接操作。 该toolchain 在执行时还指定一组参数传递到 build config 文件。 这使您可以配置信息传递给备用 toolchain。
模板是 GN 重复使用代码的主要方式。 通常, 模板会扩展到一个或多个其他目标类型。
template("idl") {
idl_target_name = "${target_name}_generate"
action_foreach(idl_target_name) {
source_set(target_name) {
deps = [ ":$idl_target_name" ]
通常情况下你的模板定义在一个.gni 文件中, 用户 import 该文件看到模板的定义:
import("//tools/idl_compiler.gni")
idl("my_interfaces") {
sources = [ "a.idl", "b.idl" ]
声明模板会在当时在范围内的变量周围创建一个闭包。 当调用模板时,魔术变量调用器用于从调用作用域读取变量。 模板通常将其感兴趣的值复制到其自己的范围中:
template("idl") {
source_set(target_name) {
sources = invoker.sources
模板执行时的当前目录将是调用构建文件的目录,而不是模板源文件。 这是从模板调用器传递的文件将是正确的(这通常说明大多数文件处理模板)。 但是,如果模板本身有文件(也许它生成一个运行脚本的动作),你将需要使用绝对路径(“// foo / …”)来引用这些文件, 当前目录在调用期间将不可预测。 有关更多信息和更完整的示例,请参阅gn帮助模板。
9.其他功能
9.1 Imports
您可以 import .gni 文件到当前文件中。 这不是 C++中的 include。 Import 的文件将独立执行并将执行的结果复制到当前文件中(C ++执行的时候, 当遇到 include 指令时才会在当前环境中 include 文件)。 Import 允许导入的结果被缓存, 并且还防止了一些“creative”的用途包括像嵌套 include 文件。通常一个.gni 文件将定义 build 的参数和模板。 命令 gn help import 查看更多信息。.gni 文件可以定义像_this 名字前使用一个下划线的临时变量, 从而它不会被传出文件外。。
9.2 路径处理
通常你想使一个文件名或文件列表名相对于不同的目录。 这在运行 scripts 时特别常见的, 当构建输出目录为当前目录执行的时候, 构建文件通常是指相对于包含他们的目录的文件。您可以使用 rebase_path 转化目录。命令 gn help rebase_path 查看纤细信息。
Patterns 被用来在一个部分表示一个或多个标签。
命令: gn help set_sources_assignment_filter
gn help label_pattern 查看更多信息。
9.4 执行脚本
有两种方式来执行脚本。 GN中的所有外部脚本都在Python中。第一种方式是构建步骤。这样的脚本将需要一些输入并生成一些输出作为构建的一部分。调用脚本的目标使用“action”目标类型声明(请参阅gn help action)。
在构建文件执行期间,执行脚本的第二种方式是同步的。在某些情况下,这是必要的,以确定要编译的文件集,或者获取构建文件可能依赖的某些系统配置。构建文件可以读取脚本的stdout并以不同的方式对其执行操作。
同步脚本执行由exec_script函数完成(有关详细信息和示例,请参阅gn help exec_script)。因为同步执行脚本需要暂停当前的buildfile执行,直到Python进程完成执行,所以依赖外部脚本很慢,应该最小化。
为了防止滥用,允许调用exec_script的文件可以在toplevel .gn文件中列入白名单。 Chrome会执行此操作,需要对此类添加进行其他代码审核。请参阅gn help dotfile。
您可以同步读取和写入在同步运行脚本时不鼓励但偶尔需要的文件。典型的用例是传递比当前平台的命令行限制更长的文件名列表。有关如何读取和写入文件,请参阅gn help read_file和gn help write_file。如果可能,应避免这些功能。
超过命令行长度限制的操作可以使用响应文件来解决此限制,而不同步写入文件。请参阅gn help response_file_contents。
10.与Blaze的区别和相似之处
Blaze是Google的内部构建系统,现在作为Bazel公开发布。它启发了许多其他系统,如Pants和buck。
在Google的同类环境中,对条件的需求非常低,他们可以通过一些hacks(abi_deps)来实现。 Chrome在所有地方使用条件,需要添加这些是文件看起来不同的主要原因。
GN还添加了“configs”的概念来管理一些棘手的依赖和配置问题,这些问题同样不会出现在服务器上。 Blaze有一个“配置”的概念,它像GN工具链,但内置到工具本身。工具链在GN中的工作方式是尝试将这个概念以干净的方式分离到构建文件中的结果。
GN保持一些GYP概念像“所有依赖”设置,在Blaze中工作方式有点不同。这部分地使得从现有GYP代码的转换更容易,并且GYP构造通常提供更细粒度的控制(其取决于情况是好还是坏)。
GN也使用像“sources”而不是“srcs”的GYP名称,因为缩写这似乎不必要地模糊,虽然它使用Blaze的“deps”,因为“dependencies”很难键入。 Chromium还在一个目标中编译多种语言,因此指定了目标名称前缀的语言类型(例如从cc_library)。
一、不得利用本站危害国家安全、泄露国家秘密,不得侵犯国家社会集体的和公民的合法权益,不得利用本站制作、复制和传播不法有害信息!
二、互相尊重,对自己的言论和行为负责。
本文标题:
本页链接: “React、Vue、Angular等均属于MVVM模式,在一些只需完成数据和模板简单渲染的场合,显得笨重且学习成本较高,而解决该问题非常优秀框架之一是doT.js,本文将对它进行详解。 ”
前端渲染有很多框架,而且形式和内容在不断发生变化。这些演变的背后是设计模式的变化,而归根到底是功能划分逻辑的演变:MVC--&MVP--&MVVM(忽略最早混在一起的写法,那不称为模式)。近几年兴起的React、Vue、Angular等框架都属于MVVM模式,能帮我们实现界面渲染、事件绑定、路由分发等复杂功能。但在一些只需完成数据和模板简单渲染的场合,它们就显得笨重而且学习成本较高了。
例如,在美团外卖的开发实践中,前端经常从后端接口取得长串的数据,这些数据拥有相同的样式模板,前端需要将这些数据在同一个样式模板上做重复渲染操作。
解决这个问题的模板引擎有很多,doT.js(出自女程序员Laura Doktorova之手)是其中非常优秀的一个。下表将doT.js与其他同类引擎做了对比:
可以看出,doT.js表现突出。而且,它的性能也很优秀,本人在Mac Pro上的用Chrome浏览器(版本为:56.0.2924.87)上做100条数据10000次渲染性能测试,结果如下:
从上可以看出doT.js更值得推荐,它的主要优势在于:
小巧精简,源代码不超过两百行,6KB的大小,压缩版只有4KB;
支持表达式丰富,涵盖几乎所有应用场景的表达式语句;
性能优秀;
不依赖第三方库。
本文主要对doT.js的源码进行分析,探究一下这类模板引擎的实现原理。
如何使用
如果之前用过doT.js,可以跳过此小节,doT.js使用示例如下:
& type="text/html" id="tpl"& &div& &a&name:{{= it.name}}&/a& &p&age:{{= it.age}}&/p& &p&hello:{{= it.sayHello() }}&/p& &select& {{~ it.arr:item}} &option {{?item.id == it.stringParams2}}selected{{?}} value="{{=item.id}}"& {{=item.text}} &/option& {{~}} &/select& &/div&&/&&& $("#app").html(doT.template($("#tpl").html())({ name:'stringParams1', stringParams1:'stringParams1_value', stringParams2:1, arr:[{id:0,text:'val1'},{id:1,text:'val2'}], sayHello:function () { return this[this.name] } }));&/&
可以看出doT.js的设计思路:将数据注入到预置的视图模板中渲染,返回HTML代码段,从而得到最终视图。
下面是一些常用语法表达式对照表:
源码分析及实现原理
和后端渲染不同,doT.js的渲染完全交由前端来进行,这样做主要有以下好处:
脱离后端渲染语言,不需要依赖后端项目的启动,从而降低了开发耦合度、提升开发效率;
View层渲染逻辑全在Java层实现,容易维护和修改;
数据通过接口得到,无需考虑后端数据模型变化,只需关心数据格式。
doT.js源码核心:
...// 去掉所有制表符、空格、换行str = ("var out='" + (c.strip ? str.replace(/(^|r|n)t* +| +t*(r|n|$)/g," ") .replace(/r|n|t|/*[sS]*?*//g,""): str) .replace(/'|/g, "$&") .replace(c.interpolate || skip, function(m, code) { return cse.start + unescape(code,c.canReturnNull) + cse. }) .replace(c.encode || skip, function(m, code) { needhtmlencode = return cse.startencode + unescape(code,c.canReturnNull) + cse. }) // 条件判断正则匹配,包括if和else判断 .replace(c.conditional || skip, function(m, elsecase, code) { return elsecase ? (code ? "';}else if(" + unescape(code,c.canReturnNull) + "){out+='" : "';}else{out+='") : (code ? "';if(" + unescape(code,c.canReturnNull) + "){out+='" : "';}out+='"); }) // 循环遍历正则匹配 .replace(c.iterate || skip, function(m, iterate, vname, iname) { if (!iterate) return "';} } out+='"; sid+=1; indv=iname || "i"+ iterate=unescape(iterate); return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"&l"+sid+"){" +vname+"=arr"+sid+"["+indv+"+=1];out+='"; }) // 可执行代码匹配 .replace(c.evaluate || skip, function(m, code) { return "';" + unescape(code,c.canReturnNull) + "out+='"; }) + "';") ...try { return new Function(c.varname, str);//c.varname 定义的是new Function()返回的函数的参数名 } catch (e) { /* istanbul ignore else */ if (typeof console !== "undefined") console.log("Could not create a template function: " + str); }...
这段代码总结起来就是一句话:用正则表达式匹配预置模板中的语法规则,将其转换、拼接为可执行HTML代码,作为可执行语句,通过new Function()创建的新方法返回。
代码解析重点1:正则替换
正则替换是doT.js的核心设计思路,本文不对正则表达式做扩充讲解,仅分析doT.js的设计思路。先来看一下doT.js中用到的正则:
templateSettings: { evaluate: /{{([sS]+?(}?)+)}}/g, //表达式 interpolate: /{{=([sS]+?)}}/g, // 插入的变量 encode: /{{!([sS]+?)}}/g, // 在这里{{!不是用来做判断,而是对里面的代码做编码 use: /{{#([sS]+?)}}/g, useParams: /(^|[^w$])def(?:.|[['"])([w$.]+)(?:['"]])?s*:s*([w$.]+|"[^"]+"|'[^']+'|{[^}]+})/g, define: /{{##s*([w.$]+)s*(:|=)([sS]+?)#}}/g,// 自定义模式 defineParams:/^s*([w$]+):([sS]+)/, // 自定义参数 conditional: /{{?(?)?s*([sS]*?)s*}}/g, // 条件判断 iterate: /{{~s*(?:}}|([sS]+?)s*:s*([w$]+)s*(?::s*([w$]+))?s*}})/g, // 遍历 varname: "it", // 默认变量名 strip: true, append: true, selfcontained: false, doNotSkipEncoded: false // 是否跳过一些特殊字符}
源码中将正则定义写到一起,这样方便了维护和管理。在早期版本的doT.js中,处理条件表达式的方式和tmpl一样,采用直接替换成可执行语句的形式,在最新版本的doT.js中,修改成仅一条正则就可以实现替换,变得更加简洁。
doT.js源码中对模板中语法正则替换的流程如下:
代码解析重点2:new Function()运用
函数定义时,一般通过Function关键字,并指定一个函数名,用以调用。在Java中,函数也是对象,可以通过函数对象(Function Object)来创建。正如数组对象对应的类型是Array,日期对象对应的类型是Date一样,如下所示:
var funcName = new Function(p1,p2,...,pn,body);
参数的数据类型都是字符串,p1到pn表示所创建函数的参数名称列表,body表示所创建函数的函数体语句,funcName就是所创建函数的名称(可以不指定任何参数创建一个匿名函数)。
下面的定义是等价的。
// 一般函数定义方式function func1(a,b){ return a+b;}// 参数是一个字符串通过逗号分隔var func2 = new Function('a,b','return a+b');// 参数是多个字符串var func3 = new Function('a','b','return a+b');// 一样的调用方式console.log(func1(1,2));console.log(func2(2,3));console.log(func3(1,3));// 输出3 // func15 // func24 // func3
从上面的代码中可以看出,Function的最后一个参数,被转换为可执行代码,类似eval的功能。eval执行时存在浏览器性能下降、调试困难以及可能引发XSS(跨站)攻击等问题,因此不推荐使用eval执行字符串代码,new Function()恰好解决了这个问题。回过头来看doT代码中的”new Function(c.varname, str)”,就不难理解varname是传入可执行字符串str的变量。
具体关于new Fcuntion的定义和用法,详细请阅读。
性能之因
读到这里可能会产生一个疑问:doT.js的性能为什么在众多引擎如此突出?通过阅读其他引擎源代码,发现了它们核心代码段中都存在这样那样的问题。
jQuery-tmpl
function buildTmplFn( markup ) { return new Function("jQuery","$item", // Use the variable __ to hold a string array while building the compiled template. (See /jquery/jquery-tmpl/issues#issue/10). "var $=jQuery,call,__=[],$data=$item." + // Introduce the data as local variables using with(){} "with($data){__.push('" + // Convert the template into pure Java jQuery.trim(markup) .replace( /(['])/g, "$1" ) .replace( /[rtn]/g, " " ) .replace( /${([^}]*)}/g, "{{= $1}}" ) .replace( /{{(/?)(w+|.)(?:(((?:[^}]|}(?!}))*?)?))?(?:s+(.*?)?)?((((?:[^}]|}(?!}))*?)))?s*}}/g, function( all, slash, type, fnargs, target, parens, args ) { //省略部分模板替换语句,若要阅读全部代码请访问:/BorisMoore/jquery-tmpl }) + "');}return __;" ); }
在上面的代码中看到,jQuery-teml同样使用了new Function()的方式编译模板,但是在性能对比中jQuery-teml性能相比doT.js相差甚远,出现性能瓶颈的关键在于with语句的使用。
with语句为什么对性能有这么大的影响?我们来看下面的代码:
var datas = {persons:['李明','小红','赵四','王五','张三','孙行者','马婆子'],gifts:['平民','巫师','狼','猎人','先知']};function go(){ with(datas){ var personIndex = 0,giftIndex = 0,i=100000; while(i){ personIndex = Math.floor(Math.random()*persons.length); giftIndex = Math.floor(Math.random()*gifts.length) console.log(persons[personIndex] +'得到了新的身份:'+ gifts[giftIndex]); i--; } }}
上面代码中使用了一个with表达式,为了避免多次从datas中取变量而使用了with语句。这看起来似乎提升了效率,但却产生了一个性能问题:在Java中执行方法时会产生一个执行上下文,这个执行上下文持有该方法作用域链,主要用于标识符解析。当代码流执行到一个with表达式时,运行期上下文的作用域链被临时改变了,一个新的可变对象将被创建,它包含指定对象的所有属性。此对象被插入到作用域链的最前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,这样访问datas的属性非常快,但是访问局部变量的速度却变慢了,所以访问代价更高了,如下图所示。
这个插件在GitHub上面介绍时,作者Boris Moore着重强调两点设计思路:
模板缓存,在模板重复使用时,直接使用内存中缓存的模板。在本文作者看来,这是一个鸡肋的功能,在实际使用中,无论是直接写在String中的模板还是从Dom获取的模板都会以变量的形式存放在内存中,变量使用得当,在页面整个生命周期内都能取到这个模板。通过源码分析之后发现jQuery-tmpl的模板缓存并不是对模板编译结果进行缓存,并且会造成多次执行渲染时产生多次编译,再加上代码with性能消耗,严重拖慢整个渲染过程。
模板标记,可以从缓存模板中取出对应子节点。这是一个不错的设计思路,可以实现数据改变只重新渲染局部界面的功能。但是我觉得:模板将渲染结果交给开发者,并渲染到界面指定位置之后,模板引擎的工作就应该结束了,剩下的对节点操作应该灵活的掌握在开发者手上。
不改变原来设计思路基础之上,尝试对源代码进行性能提升。
先保留提升前性能作为对比:
首先来我们做第一次性能提升,移除源码中with语句。
第一次提升后:
接下来第二部提升,落实Boris Moore设计理念中的模板缓存:
优化后的这一部分代码段被我们修改成了:
function buildTmplFn( markup ) { if(!compledStr){ // Convert the template into pure Java compledStr = jQuery.trim(markup) .replace( /(['])/g, "$1" ) .replace( /[rtn]/g, " " ) .replace( /${([^}]*)}/g, "{{= $1}}" ) .replace( /{{(/?)(w+|.)(?:(((?:[^}]|}(?!}))*?)?))?(?:s+(.*?)?)?((((?:[^}]|}(?!}))*?)))?s*}}/g, //省略部分模板替换语句 } return new Function("jQuery","$item", // Use the variable __ to hold a string array while building the compiled template. (See /jquery/jquery-tmpl/issues#issue/10). "var $=jQuery,call,__=[],$data=$item." + // Introduce the data as local variables using with(){} "__.push('" + compledStr + "');return __;" ) }
在doT.js源码中没有用到with这类消耗性能的语句,与此同时doT.js选择先将模板编译结果返回给开发者,这样如要重复多次使用同一模板进行渲染便不会反复编译。
仅25行的模板:tmpl
(function(){ var cache = {}; this.tmpl = function (str, data){ var fn = !/W/.test(str) ? cache[str] = cache[str] || tmpl(document.getElementById(str).innerHTML) : new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" + "with(obj){p.push('" + str .replace(/[rtn]/g, " ") .split("&%").join("t") .replace(/((^|%&)[^t]*)'/g, "$1r") .replace(/t=(.*?)%&/g, "',$1,'") .split("t").join("');") .split("%&").join("p.push('") .split("r").join("'") + "');}return p.join('');"); return data ? fn( data ) : };})();
阅读这段代码会惊奇的发现,它更像是baiduTemplate精简版。相比baiduTemplate而言,它移除了baiduTemplate的自定义语法标签的功能,使得代码更加精简,也避开了替换用户语法标签而带来的性能消耗。对于doT.js来说,性能问题的关键是with语句。
综合上述我对tmpl的源码进行移除with语句改造:
改造之前性能:
改造之后性能:
如果读者对性能对比源码比较感兴趣可以访问: 。
通过对doT.js源码的解读,我们发现:
doT.js的条件判断语法标签不直观。当开发者在使用过程中条件判断嵌套过多时,很难找到对应的结束语法符号,开发者需要自己严格规范代码书写,否则会给开发和维护带来困难。
doT.js限制开发者自定义语法标签,相比较之下baiduTemplate提供可自定义标签的功能,而baiduTemplate的性能瓶颈恰好是提供自定义语法标签的功能。
很多解决我们问题的插件的代码往往简单明了,那些庞大的插件反而存在负面影响或无用功能。技术领域有一个软件设计范式:“约定大于配置”,旨在减少软件开发人员需要做决定的数量,做到简单而又不失灵活。在插件编写过程中开发者应多注意使用场景和性能的有机结合,使用恰当的语法,尽可能减少开发者的配置,不求迎合各个场景。
本文授权转自微信公众号“美团点评技术团队”。
作者:建辉,美团外卖高级前端研发工程师,2015年加入美团点评外卖事业部。目前在前端业务增长组,主要负责运营平台搭建,主导运营活动业务。
声明:本文由入驻搜狐公众平台的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场。