当前位置: 首页 > news >正文

嵌入V8入门

本文档介绍了V8的一些关键概念,并提供了一个“ hello world”示例来帮助您开始使用V8代码。

本文档适用于希望将V8 JavaScript引擎嵌入C ++应用程序中的C ++程序员。它可以帮助您使自己的应用程序的C ++对象和方法可用于JavaScript,并使JavaScript对象和函数可用于C ++应用程序

HelloWorld

让我们看一个Hello World示例,该示例采用JavaScript语句作为字符串参数,将其作为JavaScript代码执行,然后将结果打印到标准输出中。

首先,一些关键概念:

  • isolate隔离有自己的堆的VM实例。隔离表示V8引擎的隔离实例。V8隔离体有完全不同的状态。一个隔离中的对象不能用于其他隔离中。嵌入程序可以创建多个隔离,并在多个线程中并行使用它们。在任何给定时间,最多只能由一个线程输入隔离。必须使用Locker/Unlocker API进行同步。
  • scope:堆栈分配类,为本地范围内执行的所有操作设置隔离。
  • local handle本地句柄是指向对象的指针。使用句柄可以访问所有V8对象。由于V8垃圾收集器的工作方式,它们是必需的。
  • handle scope:可以将句柄作用域视为任意数量的句柄的容器。完成句柄后,不必删除单个的句柄,只需删除它们的作用域即可
  • context上下文是一种执行环境,它允许单独的,不相关的JavaScript代码在V8的单个实例中运行。您必须明确指定要在其中运行任何JavaScript代码的上下文。

在高级指南中将更详细地讨论这些概念。

运行示例

请按照以下步骤自行运行示例:

  1. 按照Git指示下载V8源代码。

  2. HelloWorld示例示例的说明最近已通过V8 v7.1.11进行了测试。您可以使用git checkout refs/tags/7.1.11 -b sample -t

  3. 使用帮助程序脚本创建构建配置:

    tools/dev/v8gen.py x64.release.sample

    您可以通过运行以下命令检查并手动编辑构建配置:

    gn args out.gn/x64.release.sample
  4. 在Linux 64系统上构建静态库:

    ninja -C out.gn/x64.release.sample v8_monolith
  5. 编译hello-world.cc,链接到在构建过程中创建的静态库。例如,在使用GNU编译器的64位Linux上:

    g++ -I. -Iinclude samples/hello-world.cc -o hello_world -lv8_monolith -Lout.gn/x64.release.sample/obj/ -pthread -std
  6. 对于更复杂的代码,如果没有ICU数据文件,V8将失败。将此文件复制到二进制文件的存储位置:

    cp out.gn/x64.release.sample/icudtl.dat
  7. hello_world在命令行中运行可执行文件。例如,在Linux上的V8目录中,运行:

    ./hello_world
  8. 它打印Hello, World!。好极了!

如果您正在寻找与master同步的示例,请签出该文件hello-world.cc。这是一个非常简单的示例,您可能要做的不仅仅是将脚本作为字符串执行。以下高级指南包含有关V8嵌入程序的更多信息。

更多示例代码

作为源代码下载的一部分,提供了以下示例。

process.cc

此示例提供了扩展假设的HTTP请求处理应用程序所必需的代码(例如,它可能是Web服务器的一部分),以便可以编写脚本。它以JavaScript脚本作为参数,必须提供一个名为的函数Process。JavaScriptProcess函数可用于例如收集信息,例如虚构的Web服务器所服务的每个页面获得多少点击。

shell.cc

此示例将文件名作为参数,然后读取并执行其内容。包括命令提示符,您可以在其中输入JavaScript代码段,然后执行这些代码段。在此示例print中,还通过使用对象和函数模板将其他功能添加到了JavaScript。

进阶指南

现在您已经熟悉了将V8用作独立虚拟机以及一些关键的V8概念(例如句柄,作用域和上下文),让我们进一步讨论这些概念,并介绍一些其他一些概念,这些概念对于将V8嵌入您自己的C ++至关重要应用。

V8 API提供了用于编译和执行脚本访问C ++方法和数据结构处理错误以及启用安全检查的功能。您的应用程序可以像其他任何C ++库一样使用V8。您的C ++代码通过包含header来通过V8 API访问V8 include/v8.h

处理和垃圾收集

句柄提供对JavaScript对象在堆中位置的引用。V8垃圾回收器回收无法再访问的对象使用的内存。在垃圾收集过程中,垃圾收集器通常将对象移动到堆中的不同位置。当垃圾收集器移动对象时,垃圾收集器还会使用对象的新位置来更新所有引用该对象的句柄。

如果无法从JavaScript访问对象并且没有引用该对象的句柄,则该对象被视为垃圾。垃圾收集器会不时删除所有被视为垃圾的对象。V8的垃圾回收机制是V8性能的关键

有几种类型的句柄:

  • 局部句柄保存在堆栈中,并在调用适当的析构函数时被删除。这些句柄的生存期由句柄范围(HandleScope)决定,该范围通常是在函数调用开始时创建的。删除句柄作用域后,垃圾回收器可以自由地释放以前由句柄作用域中的句柄引用的那些对象,前提是它们不再可从JavaScript或其他句柄访问。上面的hello world示例中使用了这种类型的句柄。

    局部句柄具有类Local<SomeType>

    注意:句柄堆栈不是C ++调用堆栈的一部分,但句柄作用域已嵌入C ++堆栈中。句柄作用域只能通过堆栈分配,而不能使用进行分配new

  • 持久句柄提供了对堆分配的JavaScript对象的引用,就像本地句柄一样。有两种类型,它们处理的参考的生存期管理不同。当您需要为一个以上的函数调用保留对一个对象的引用时,或者当句柄寿命不对应于C ++范围时,请使用持久句柄。例如,谷歌浏览器使用持久性句柄来引用文档对象模型(DOM)节点。PersistentBase::SetWeak当对对象的唯一引用来自弱持久句柄时,可以使用来使持久句柄变弱,以从垃圾回收器触发回调。

    • UniquePersistent<SomeType>句柄依赖于C ++构造和析构来管理底层对象的寿命。
    • Persistent<SomeType>可以使用其构造函数来构造A,但必须使用明确将其清除Persistent::Reset
  • 还有一些其他类型的句柄很少使用,这里我们仅简要介绍一下:

    • Eternal是用于永远不会删除的JavaScript对象的持久句柄。使用起来更便宜,因为它使垃圾收集器不必确定该对象的活动性。
    • PersistentUniquePersistent不能被复制,这使得它们不适合作为具有pre-C++11标准库的容器的值。PersistentValueMapPersistentValueVector提供持久性值的容器类,并具有类似map和vector的语义。C ++ 11嵌入器不需要这些,因为C ++ 11的移动语义解决了根本的问题。

当然,每次创建对象时创建本地句柄都会导致很多句柄!这是句柄作用域非常有用的地方。您可以将句柄作用域视为包含大量句柄的容器调用句柄作用域的析构函数时,将从堆栈中删除在该作用域内创建的所有句柄。如您所料,这导致句柄指向的对象可以被垃圾收集器从堆中删除。

回到我们非常简单的hello world示例,在下图中,您可以看到句柄堆栈和堆分配的对象。请注意,这Context::New()将返回一个Local句柄,并且我们将Persistent基于该句柄创建一个新的句柄以演示Persistent句柄的用法。

HandleScope::~HandleScope调用析构函数时,将删除句柄作用域。如果没有其他引用,则在删除的句柄范围内由句柄引用的对象可以在下一个垃圾回收中删除。垃圾回收器还可以从堆中删除source_objscript_obj对象,因为它们不再被任何句柄引用或无法通过JavaScript访问。由于上下文句柄是持久句柄,因此退出句柄作用域时不会将其删除。删除上下文句柄的唯一方法是显式调用Reset它。局部句柄由HandleScope进行管理,不需要显示删除;持久句柄由垃圾收集器管理,需要显示删除。

注意:在本文档中,术语“句柄”是指局部句柄。在讨论持久句柄时,该术语将完整使用。

重要的是,请注意此模型的一个常见陷阱:您不能直接从声明了句柄作用域的函数中返回局部句柄。如果执行本地句柄,则尝试返回的结果最终会在函数返回之前被句柄作用域的析构函数删除。返回局部句柄的正确方法是构造一个EscapableHandleScope而不是HandleScope,并在句柄范围内调用Escape方法,并传入要返回其值的句柄。这是一个在实践中如何工作的示例:

// This function returns a new array with three elements, x, y, and z.
Local<Array> NewPointArray(int x, int y, int z) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  // We will be creating temporary handles so we use a handle scope.
  v8::EscapableHandleScope handle_scope(isolate);
  // Create a new empty array.
  v8::Local<v8::Array> array = v8::Array::New(isolate, 3);
  // Return an empty result if there was an error creating the array.
  if (array.IsEmpty())
    return v8::Local<v8::Array>();
  // Fill out the values
  array->Set(0, Integer::New(isolate, x));
  array->Set(1, Integer::New(isolate, y));
  array->Set(2, Integer::New(isolate, z));
  // Return the value through Escape.
  return handle_scope.Escape(array);
}

Escape方法将其参数的值复制到封闭范围内,删除其所有本地句柄,然后返回可以安全返回的新句柄副本

Contexts

在V8中,上下文是一个执行环境,它允许单独的,不相关的JavaScript应用程序在V8的单个实例中运行。您必须明确指定要在其中运行任何JavaScript代码的上下文

为什么这是必要的?因为JavaScript提供了一组内置的实用程序函数和对象,所以可以通过JavaScript代码对其进行更改。例如,如果两个完全不相关的JavaScript函数都以相同的方式更改了全局对象,那么很可能会发生意外结果。

在CPU时间和内存方面,鉴于必须构建的内置对象的数量,创建新的执行上下文似乎是一项昂贵的操作。但是,V8广泛的缓存确保了,尽管您创建的第一个上下文有些昂贵,但随后的上下文要便宜得多。这是因为第一个上下文需要创建内置对象并解析内置JavaScript代码,而后续上下文仅需要为其上下文创建内置对象。具有V8快照功能(通过构建选项激活snapshot=yes,这是默认设置),用于快照创建的第一个上下文的时间将得到高度优化,因为快照包括序列化堆,该堆包含已为内置JavaScript代码编译的代码。与垃圾回收一起,V8的广泛缓存也是V8性能的关键。

创建上下文后,可以多次输入和退出它。在上下文A中时,您还可以输入其他上下文B,这意味着您将B替换为A作为当前上下文。退出B时,A将还原为当前上下文。如下图所示:

 

请注意,每个上下文的内置实用程序功能和对象保持独立。创建上下文时,可以选择设置安全令牌。有关更多信息,请参见安全模型部分。

在V8中使用上下文的动机是使浏览器中的每个窗口和iframe可以拥有自己的全新JavaScript环境。

Templates

模板是上下文中JavaScript函数和对象的蓝图。您可以使用模板将C ++函数和数据结构包装在JavaScript对象中,以便可以由JavaScript脚本操纵它们。例如,谷歌浏览器使用模板将C ++ DOM节点包装为JavaScript对象,并在全局名称空间中安装函数。您可以创建一组模板,然后对创建的每个新上下文使用相同的模板。您可以根据需要拥有任意数量的模板。但是,在任何给定的上下文中,您只能拥有任何模板的一个实例。

在JavaScript中,函数和对象之间具有很强的二元性。要使用Java或C ++创建新的对象类型,通常需要定义一个新类。在JavaScript中,您可以创建一个新函数,然后使用该函数作为构造函数来创建实例。JavaScript对象的布局和功能与构造它的功能紧密相关。V8模板的工作方式反映了这一点。模板有两种类型:

  • 函数模板(Function templates)

    函数模板是单个函数的蓝图。通过GetFunction在要实例化JavaScript函数的上下文中调用模板的方法,可以创建模板的JavaScript实例。您还可以将C ++回调与函数模板相关联,该函数模板在调用JavaScript函数实例时被调用。

  • 对象模板(Object templates)

    每个函数模板都有一个关联的对象模板。这用于配置使用此函数创建的对象作为其构造函数。您可以将两种类型的C ++回调与对象模板相关联:

    • 当脚本访问特定对象属性时,将调用访问器回调
    • 脚本访问任何对象属性时,将调用拦截器回调

访问器和拦截器将在本文档后面讨论。

以下代码提供了为全局对象创建模板并设置内置全局函数的示例。

// Create a template for the global object and set the
// built-in global functions.
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);
global->Set(v8::String::NewFromUtf8(isolate, "log"), v8::FunctionTemplate::New(isolate, LogCallback));
// Each processor gets its own context so different processors
// do not affect each other.
v8::Persistent<v8::Context> context = v8::Context::New(isolate, nullptr, global);

此示例代码取自JsHttpProcessor::Initializerprocess.cc

Accessors

访问器是一个C++回调,它通过JavaScript脚本访问对象属性,计算并返回一个值。使用该SetAccessor方法,通过对象模板配置访问器。该方法采用与之关联的属性的名称,并在脚本尝试读取或写入该属性时运行两个回调。

访问器的复杂性取决于您要处理的数据类型:

  • 访问静态全局变量
  • 访问动态变量

访问静态全局变量

比方说,有两个C ++整型变量,xy这是提供给JavaScript作为一个上下文中的全局变量。为此,每当脚本读取或写入这些变量时,就需要调用C ++访问器函数。这些访问器函数使用将C ++整数Integer::New转换为JavaScript整数,并使用将JavaScript整数转换为C ++整数Int32Value。下面提供了一个示例:

//getter
void XGetter(v8::Local<v8::String> property, const v8::PropertyCallbackInfo<Value>& info) {
  info.GetReturnValue().Set(x);
}
//setter
void XSetter(v8::Local<v8::String> property, v8::Local<v8::Value> value, const v8::PropertyCallbackInfo<void>& info) {
  x = value->Int32Value();
}
// YGetter/YSetter are so similar they are omitted for brevity
v8::Local<v8::ObjectTemplate> global_templ = v8::ObjectTemplate::New(isolate);
global_templ->SetAccessor(v8::String::NewFromUtf8(isolate, "x"), XGetter, XSetter);
global_templ->SetAccessor(v8::String::NewFromUtf8(isolate, "y"), YGetter, YSetter);
v8::Persistent<v8::Context> context = v8::Context::v8::New(isolate, nullptr, global_templ);

请注意,上面代码中的对象模板是与上下文同时创建的。该模板可以事先创建,然后用于任何数量的上下文。

Accessing dynamic variables

在前面的示例中,变量是静态的和全局的。如果被操纵的数据是动态的,就像浏览器中的DOM树一样,会怎么样?让我们想象X和Y是C++类点上的对象字段:

class Point {
 public:
  Point(int x, int y) : x_(x), y_(y) { }
  int x_, y_;
}

为了使任意数量的C++Point实例对JavaScript可用,我们需要为每个C++点创建一个JavaScript对象,并在JavaScript对象和C++实例之间建立连接。这是通过外部值和内部对象字段完成的。

首先为Point包装器对象创建对象模板:

v8::Local<v8::ObjectTemplate> point_templ = v8::ObjectTemplate::New(isolate);

每个JavaScript point对象都保留对C ++对象的引用,对于该对象,它是带有内部字段的包装器之所以命名这些字段是因为无法从JavaScript内访问它们,而只能从C ++代码访问它们。一个对象可以有任意数量的内部字段,内部字段的数量在对象模板上设置如下:

point_templ->SetInternalFieldCount(1);

此处将内部字段计数设置为1,这意味着该对象具有一个内部字段,其索引为0,它指向C ++对象。

xy访问器添加到模板中:

point_templ->SetAccessor(v8::String::NewFromUtf8(isolate, "x"), GetPointX, SetPointX);
point_templ->SetAccessor(v8::String::NewFromUtf8(isolate, "y"), GetPointY, SetPointY);

接下来,通过创建模板的新实例包装C ++ Point,然后将内部字段设置为0围绕该point 的外部包装器p

Point* p = ...;
v8::Local<v8::Object> obj = point_templ->NewInstance();
obj->SetInternalField(0, v8::External::New(isolate, p));

外部对象是围绕void*的包装器。外部对象只能用于在内部字段中存储引用值。JavaScript对象不能直接引用C ++对象,因此外部值用作从JavaScript到C ++的“桥梁”。在这种意义上,外部值与句柄相反,因为句柄使C ++可以引用JavaScript对象。

这是的getset访问器的定义xy访问器定义相同,除了y代替x

void GetPointX(Local<String> property, const PropertyCallbackInfo<Value>& info) {
  v8::Local<v8::Object> self = info.Holder();
  v8::Local<v8::External> wrap = v8::Local<v8::External>::Cast(self->GetInternalField(0));
  void* ptr = wrap->Value();
  int value = static_cast<Point*>(ptr)->x_;
  info.GetReturnValue().Set(value);
}
void SetPointX(v8::Local<v8::String> property, v8::Local<v8::Value> value, const v8::PropertyCallbackInfo<void>& info) {
  v8::Local<v8::Object> self = info.Holder();
  v8::Local<v8::External> wrap = v8::Local<v8::External>::Cast(self->GetInternalField(0));
  void* ptr = wrap->Value();
  static_cast<Point*>(ptr)->x_ = value->Int32Value();
}

访问器提取对point由JavaScript对象包装的对象的引用,然后读取和写入关联的字段。这样,这些通用访问器可以在任意数量的包装点对象上使用。

Interceptors

您也可以在脚本访问任何对象属性时指定回调。这些被称为拦截器。为了提高效率,有两种类型的拦截器:

  • 命名属性拦截器-在访问具有字符串名称的属性时调用。
    在浏览器环境中,这是一个示例document.theFormName.elementName
  • 索引属性拦截器-访问索引属性时调用。在浏览器环境中,这是一个示例document.forms.elements[0]

例子process.cc,设置有V8源代码,包括使用拦截器的一个例子。在以下代码段中,SetNamedPropertyHandler指定MapGetMapSet拦截器:

v8::Local<v8::ObjectTemplate> result = v8::ObjectTemplate::New(isolate);
result->SetNamedPropertyHandler(MapGet, MapSet);

MapGet拦截器提供如下:

void JsHttpRequestProcessor::MapGet(v8::Local<v8::String> name, const v8::PropertyCallbackInfo<Value>& info) {
  // Fetch the map wrapped by this object.
  map<string, string> *obj = UnwrapMap(info.Holder());
  // Convert the JavaScript string to a std::string.
  string key = ObjectToString(name);
  // Look up the value if it exists using the standard STL idiom.
  map<string, string>::iterator iter = obj->find(key);
  // If the key is not present return an empty handle as signal.
  if (iter == obj->end()) return;
  // Otherwise fetch the value and wrap it in a JavaScript string.
  const string &value = (*iter).second;
  info.GetReturnValue().Set(v8::String::NewFromUtf8(value.c_str(), v8::String::kNormalString, value.length()));
}

与访问器一样,只要访问属性,就会调用指定的回调。访问器和拦截器之间的区别在于,拦截器处理所有属性,而访问器则与一个特定的属性相关联。

Security model

相同来源策略”(首次与Netscape Navigator 2.0一起引入)可防止从一个“来源”加载的文档或脚本从另一个“来源”获取或设置文档的属性。术语“来源”在此定义为域名(例如www.example.com),协议(例如https)和端口的组合。例如,www.example.com:81与的来源不同www.example.com。所有这三个网页都必须匹配,才能将两个网页视为具有相同的来源。没有这种保护,恶意网页可能会损害另一个网页的完整性。

在V8中,“来源”定义为上下文。默认情况下,不允许访问除要调用的上下文以外的任何上下文。要访问不同于您要调用的上下文,您需要使用安全性令牌或安全性回调。安全令牌可以是任何值,但通常是符号,在其他任何地方都不存在的规范字符串。您可以选择SetSecurityToken在设置上下文时指定安全令牌。如果未指定安全令牌,则V8会自动为您创建的上下文生成一个。

尝试访问全局变量时,V8安全系统首先将尝试访问全局对象的代码的安全令牌与正在访问的全局对象的安全令牌进行比较。如果令牌匹配,则授予访问权限。如果令牌不匹配,则V8执行回调以检查是否应允许访问。您可以使用以下命令通过在对象上设置安全性回调来指定是否应允许访问对象:SetAccessCheckCallbacks对象模板上的方法。然后,V8安全系统可以获取正在访问的对象的安全回调,并调用它以询问是否允许另一个上下文访问它。给该回调函数提供要访问的对象,要访问的属性的名称,访问的类型(例如,读取,写入或删除),并返回是否允许访问。

该机制在谷歌浏览器中实现,这样,如果安全令牌不匹配,一个特殊的回调来只允许以下访问:window.focus()window.blur()window.close()window.locationwindow.open()history.forward()history.back(),和history.go()

Exceptions

如果发生错误(例如,脚本或函数试图读取不存在的属性,或者调用的函数不是函数),则V8会引发异常。

如果操作未成功,则V8返回空句柄。因此,在继续执行之前,代码必须检查返回值不是空句柄,这一点很重要。使用Local类的public成员函数检查空句柄IsEmpty()

您可以使用捕获异常TryCatch,例如:

v8::TryCatch trycatch(isolate);
v8::Local<v8::Value> v = script->Run();
if (v.IsEmpty()) {
  v8::Local<v8::Value> exception = trycatch.Exception();
  v8::String::Utf8Value exception_str(exception);
  printf("Exception: %s\n", *exception_str);
  // ...
}

如果返回的值是一个空句柄,并且您没有TryCatch适当的位置,则您的代码必须进行紧急救援。如果确实有TryCatch异常,则会捕获到异常,并允许您的代码继续进行处理。

Inheritance

JavaScript是一种,面向对象的语言,因此,它使用原型继承而不是经典继承。对于接受过传统的面向对象语言(如C ++和Java)训练的程序员来说,这可能会令人困惑。

基于类的面向对象的语言(例如Java和C ++)基于两个不同的实体(类和实例)的概念而建立。JavaScript是基于原型的语言,因此没有区别:它只是具有对象。JavaScript本身不支持类层次结构的声明。但是,JavaScript的原型机制简化了向对象的所有实例添加自定义属性和方法的过程。在JavaScript中,您可以向对象添加自定义属性。例如:

// Create an object named `bicycle`.
function bicycle() {}
// Create an instance of `bicycle` called `roadbike`.
var roadbike = new bicycle();
// Define a custom property, `wheels`, on `roadbike`.
roadbike.wheels = 2;

以这种方式添加的自定义属性仅对于该对象的实例存在。如果我们创建的另一个实例bicycle()mountainbike例如,mountainbike.wheels它将返回,undefined除非wheels明确添加了该属性。

有时候,这正是所需要的,而在其他时候,将custom属性添加到对象的所有实例将很有帮助-毕竟所有自行车都有轮子。这是JavaScript原型对象非常有用的地方。要使用原型对象,请prototype在对象上引用关键字,然后再向其添加自定义属性,如下所示:

// First, create the “bicycle” object
function bicycle() {}
// Assign the wheels property to the object’s prototype
bicycle.prototype.wheels = 2;

bicycle()现在,所有实例将具有wheels预内置的属性。

在带有模板的V8中使用了相同的方法。每个方法FunctionTemplate都有一个PrototypeTemplate为函数原型提供模板的方法。您可以设置属性,并将C ++函数与这些属性相关联,PrototypeTemplate然后在上将出现在相应属性的所有实例上FunctionTemplate。例如:

v8::Local<v8::FunctionTemplate> biketemplate = v8::FunctionTemplate::New(isolate);
biketemplate->PrototypeTemplate().Set(
    v8::String::NewFromUtf8(isolate, "wheels"),
    v8::FunctionTemplate::New(isolate, MyWheelsMethodCallback)->GetFunction()
);

这导致的所有实例在其原型链中biketemplate都有一个wheels方法,该方法在被调用时会导致C ++函数MyWheelsMethodCallback被调用。

V8的FunctionTemplate类提供了公共成员函数Inherit(),当您希望一个函数模板从另一个函数模板继承时,可以调用该成员函数,如下所示:

void Inherit(v8::Local<v8::FunctionTemplate> parent);

 

相关文章:

  • V8编程详解
  • 代理服务器
  • sqlite加密
  • x86/x64/x86_64/i386/ia32/ia64/amd/amd64 辨析
  • 理清gcc、libc、libstdc++的关系
  • gcc/g++/clang/cl编译器
  • 深入浅出让你理解什么是LLVM
  • Ninja - chromium核心构建工具
  • depot_tools
  • 智能指针 unique_ptr 详解
  • C++11中“= delete;“的使用
  • C++Error2208:...尝试引用已删除的函数
  • Ninja 构建系统
  • ICU
  • 交叉编译详解
  • 【mysql】环境安装、服务启动、密码设置
  • 30天自制操作系统-2
  • CAP 一致性协议及应用解析
  • django开发-定时任务的使用
  • nodejs实现webservice问题总结
  • PaddlePaddle-GitHub的正确打开姿势
  • passportjs 源码分析
  • PHP 7 修改了什么呢 -- 2
  • SpingCloudBus整合RabbitMQ
  • Web标准制定过程
  • windows下使用nginx调试简介
  • 从零开始在ubuntu上搭建node开发环境
  • 从伪并行的 Python 多线程说起
  • 搞机器学习要哪些技能
  • 紧急通知:《观止-微软》请在经管柜购买!
  • 让你的分享飞起来——极光推出社会化分享组件
  • 如何打造100亿SDK累计覆盖量的大数据系统
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 山寨一个 Promise
  • 适配iPhoneX、iPhoneXs、iPhoneXs Max、iPhoneXr 屏幕尺寸及安全区域
  • 网页视频流m3u8/ts视频下载
  • 我从编程教室毕业
  • LIGO、Virgo第三轮探测告捷,同时探测到一对黑洞合并产生的引力波事件 ...
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • (1)(1.11) SiK Radio v2(一)
  • (11)MATLAB PCA+SVM 人脸识别
  • (4)Elastix图像配准:3D图像
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (编译到47%失败)to be deleted
  • (附源码)springboot美食分享系统 毕业设计 612231
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (论文阅读31/100)Stacked hourglass networks for human pose estimation
  • (免费领源码)python#django#mysql校园校园宿舍管理系统84831-计算机毕业设计项目选题推荐
  • (转)C#开发微信门户及应用(1)--开始使用微信接口
  • (转)Linux下编译安装log4cxx
  • (转)为C# Windows服务添加安装程序