概述

相信很多的人,每天在终端不止一遍的执行着node这条命令,对于很多人来说,它就像一个黑盒,并不知道背后到底发生了什么,本文将会为大家揭开这个神秘的面纱,由于本人水平有限,所以只是讲一个大概其,主要关注的过程就是node模块的初始化,event loopv8的部分基本没有深入,这些部分可以关注一下我以后的文章。(提示本文非常的长,希望大家不要看烦~)

从官网学习Node.js Process模块方法速查

node是什么?

这个问题很多人都会回答就是v8 + libuv,但是除了这个两个库以外node还依赖许多优秀的开源库,可以通过process.versions来看一下:

  • http_parser主要用于解析http数据包的模块,在这个库的作者也是ry,一个纯c的库,无任何依赖
  • v8这个大家就非常熟悉了,一个优秀的js引擎
  • uv这个就是ry实现的libuv,其封装了libevIOCP,实现了跨平台,node中的i/o就是它,尽管js是单线程的,但是libuv并不是,其有一个线程池来处理这些i/o操作。
  • zlib主要来处理压缩操作,诸如熟悉的gzip操作
  • aresc-ares,这个库主要用于解析dns,其也是异步的
  • modules就是node的模块系统,其遵循的规范为commonjs,不过node也支持了ES模块,不过需要加上参数并且文件名后缀需要为mjs,通过源码看,nodeES模块的名称作为了一种url来看待,具体可以参见这里
  • nghttp2如其名字一样,是一个http2的库
  • napi是在node8出现,node10稳定下来的,可以给编写node原生模块更好的体验(终于不用在依赖于nan,每次更换node版本还要重新编译一次了)
  • openssl非常著名的库,tls模块依赖于这个库,当然还包括https
  • icu就是small-icu,主要用于解决跨平台的编码问题,versions对象中的unicodecldrtz也源自icu,这个的定义可以参见这里

从这里可以看出的是process对象在node中非常的重要,个人的理解,其实node与浏览器端最主要的区别,就在于这个process对象

注:node只是用v8来进行js的解析,所以不一定非要依赖v8,也可以用其他的引擎来代替,比如利用微软的ChakraCore,对应的node仓库

node初始化

经过上面的一通分析,对node的所有依赖有了一定的了解,下面来进入正题,看一下node的初始化过程:

挖坑

node_main.cc为入口文件,可以看到的是除了调用了node::Start之外,还做了两件事情:

NODE_SHARED_MODE忽略SIGPIPE信号

SIGPIPE信号出现的情况一般在socket收到RST packet之后,扔向这个socket写数据时产生,简单来说就是clientserver发请求,但是这时候client已经挂掉,这时候就会产生SIGPIPE信号,产生这个信号会使server端挂掉,其实node::PlatformInit中也做了这种操作,不过只是针对non-shared lib build

改变缓冲行为

stdout的默认缓冲行为为_IOLBF(行缓冲),但是对于这种来说交互性会非常的差,所以将其改为_IONBF(不缓冲)

探索

node.cc文件中总共有三个Start函数,先从node_main.cc中掉的这个Start函数开始看:


int Start(int argc, char** argv) {
// 退出之前终止libuv的终端行为,为正常退出的情况
atexit([] () { uv_tty_reset_mode(); });
// 针对平台进行初始化
PlatformInit();
// ...
Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);
// ...
v8_platform.Initialize(v8_thread_pool_size);
// 熟悉的v8初始化函数
V8::Initialize();
// ..
const int exit_code =
Start(uv_default_loop(), argc, argv, exec_argc, exec_argv);
}

上面函数只保留了一些关键不走,先来看看PlatformInit

PlatfromInit

unix中将一切都看作文件,进程启动时会默认打开三个i/o设备文件,也就是stdin stdout stderr,默认会分配0 1 2三个描述符出去,对应的文件描述符常量为STDIN_FILENO STDOUT_FILENO STDERR_FILENO,而windows中没有文件描述符的这个概念,对应的是句柄,PlatformInit首先是检查是否将这个三个文件描述符已经分配出去,若没有,则利用open("/dev/null", O_RDWR)分配出去,对于windows做了同样的操作,分配句柄出去,而且windows只做了这一个操作;对于unix来说还会针对SIGINT(用户调用Ctrl-C时发出)和SIGTERMSIGTERMSIGKILL类似,但是不同的是该信号可以被阻塞和处理,要求程序自己退出)信号来做一些特殊处理,这个处理与正常退出时一样;另一个重要的事情就是下面这段代码:


struct rlimit lim;
// soft limit 不等于 hard limit, 意味着可以增加
if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) {
// Do a binary search for the limit.
rlim_t min = lim.rlim_cur;
rlim_t max = 1 << 20;
// But if there's a defined upper bound, don't search, just set it.
if (lim.rlim_max != RLIM_INFINITY) {
min = lim.rlim_max;
max = lim.rlim_max;
}
do {
lim.rlim_cur = min + (max - min) / 2;
// 对于mac来说 hard limit 为unlimited
// 但是内核有限制最大的文件描述符,超过这个限制则设置失败
if (setrlimit(RLIMIT_NOFILE, &lim)) {
max = lim.rlim_cur;
} else {
min = lim.rlim_cur;
}
} while (min + 1 < max);
}

这件事情也就是提高一个进程允许打开的最大文件描述符,但是在mac上非常的奇怪,执行ulimit -H -n得到hard limitunlimited,所以我认为mac上的最大文件描述符会被设置为1 << 20,但是最后经过实验发现最大只能为24576,非常的诡异,最后经过一顿搜索,查到了原来mac的内核对能打开的文件描述符也有限制,可以用sysctl -A | grep kern.maxfiles进行查看,果然这个数字就是24576

Init

Init函数调用了RegisterBuiltinModules


// node.cc
void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
NODE_BUILTIN_MODULES(V)
#undef V
} // node_internals.h
#define NODE_BUILTIN_MODULES(V) \
NODE_BUILTIN_STANDARD_MODULES(V) \
NODE_BUILTIN_OPENSSL_MODULES(V) \
NODE_BUILTIN_ICU_MODULES(V)

从名字也可以看出上面的过程是进行c++模块的初始化,node利用了一些宏定义的方式,主要关注NODE_BUILTIN_STANDARD_MODULES这个宏:


#define NODE_BUILTIN_STANDARD_MODULES(V) \
V(async_wrap) \
V(buffer)
...

结合上面的定义,可以得出编译后的代码大概为:


void RegisterBuiltinModules() {
_register_async_wrap();
_register_buffer();
}

而这些_register又是从哪里来的呢?以buffer来说,对应c++文件为src/node_buffer.cc,来看这个文件的最后一行,第二个参数是模块的初始化函数:


NODE_BUILTIN_MODULE_CONTEXT_AWARE(buffer, node::Buffer::Initialize)

这个宏存在于node_internals.h中:


#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags)
static node::node_module _module = {
NODE_MODULE_VERSION,
flags,
nullptr,
__FILE__,
nullptr,
(node::addon_context_register_func) (regfunc),// 暴露给js使用的模块的初始化函数
NODE_STRINGIFY(modname),
priv,
nullptr
};
void _register_ ## modname() {
node_module_register(&amp;_module);
} #define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc)
NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)

发现调用的_register_buffer实质上调用的是node_module_register(&_module),每一个c++模块对应的为一个node_module结构体,再来看看node_module_register发生了什么:


extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast&lt;struct node_module*&gt;(m); if (mp-&gt;nm_flags &amp; NM_F_BUILTIN) {
mp-&gt;nm_link = modlist_builtin;
modlist_builtin = mp;
}
...
}

由此可以见,c++模块被存储在了一个链表中,后面process.binding()本质上就是在这个链表中查找对应c++模块,node_module是链表中的一个节点,除此之外Init还初始化了一些变量,这些变量基本上都是取决于环境变量用getenv获得即可

v8初始化

到执行完Init为止,还没有涉及的jsc++的交互,在将一些环境初始化之后,就要开始用v8这个大杀器了,v8_platform是一个结构体,可以理解为是node对于v8v8::platform一个封装,紧接着的就是对v8进行初始化,自此开始具备了与js进行交互的能力,初始化v8之后,创建了一个libuv事件循环就进入了下一个Start函数

第二个Start函数


inline int Start(uv_loop_t* event_loop,
int argc, const char* const* argv,
int exec_argc, const char* const* exec_argv) {
std::unique_ptr&lt;ArrayBufferAllocator, decltype(&amp;FreeArrayBufferAllocator)&gt;
allocator(CreateArrayBufferAllocator(), &amp;FreeArrayBufferAllocator);
Isolate* const isolate = NewIsolate(allocator.get());
// ...
{
Locker locker(isolate);
Isolate::Scope isolate_scope(isolate);
HandleScope handle_scope(isolate);
}
}

首先创建了一个v8Isolate(隔离),隔离在v8中非常常见,仿佛和进程一样,不同隔离不共享资源,有着自己得堆栈,但是正是因为这个原因在多线程的情况下,要是对每一个线程都创建一个隔离的话,那么开销会非常的大(可喜可贺的是node有了worker_threads),这时候可以借助Locker来进行同步,同时也保证了一个Isolate同一时刻只能被一个线程使用;下面两行就是v8的常规套路,下一步一般就是创建一个Context(最简化的一个流程可以参见v8hello world),HandleScope叫做句柄作用域,一般都是放在函数的开头,来管理函数创建的一些句柄(水平有限,暂时不深究,先挖个坑);第二个Start的主要流程就是这个,下面就会进入最后一个Start函数,这个函数可以说是非常的关键,会揭开所有的谜题

解开谜题


inline int Start(Isolate* isolate, IsolateData* isolate_data,
int argc, const char* const* argv,
int exec_argc, const char* const* exec_argv) {
HandleScope handle_scope(isolate);
// 常规套路
Local&lt;Context&gt; context = NewContext(isolate);
Context::Scope context_scope(context);
Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);
// ...

可以见到v8的常见套路,创建了一个上下文,这个上下文就是js的执行环境,Context::Scope是用来管理这个ContextEnvironment可以理解为一个node的运行环境,记录了isolate,event loop等,Start的过程主要是做了一些libuv的初始化以及process对象的定义:


auto process_template = FunctionTemplate::New(isolate());
process_template-&gt;SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process")); auto process_object =
process_template-&gt;GetFunction()-&gt;NewInstance(context()).ToLocalChecked();
set_process_object(process_object); SetupProcessObject(this, argc, argv, exec_argc, exec_argv);

SetupProcessObject生成了一个c++层面上的process对象,这个已经基本上和平时node中的process对象一致,但是还会有一些出入,比如没有binding等,完成了这个过程之后就开始了LoadEnvironment

LoadEnvironment


Local&lt;String&gt; loaders_name =
FIXED_ONE_BYTE_STRING(env-&gt;isolate(), "internal/bootstrap/loaders.js");
MaybeLocal&lt;Function&gt; loaders_bootstrapper =
GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
Local&lt;String&gt; node_name =
FIXED_ONE_BYTE_STRING(env-&gt;isolate(), "internal/bootstrap/node.js");
MaybeLocal&lt;Function&gt; node_bootstrapper =
GetBootstrapper(env, NodeBootstrapperSource(env), node_name);

先将lib/internal/bootstrap文件夹下的两个文件读进来,然后利用GetBootstrapper来执行js代码分别得到了一个函数,一步步来看,先看看GetBootstrapper为什么可以执行js代码,查看这个函数可以发现主要是因为ExecuteString


MaybeLocal&lt;v8::Script&gt; script =
v8::Script::Compile(env-&gt;context(), source, &amp;origin);
...
MaybeLocal&lt;Value&gt; result = script.ToLocalChecked()-&gt;Run(env-&gt;context());

这个主要利用了v8的能力,对js文件进行了解析和执行,打开loaders.js看看其参数,需要五个,捡两个最重要的来说,分别是processgetBinding,这里面往后继续看LoadEnvironment发现process对象就是刚刚生成的,而getBinding是函数GetBinding


node_module* mod = get_builtin_module(*module_v);
Local&lt;Object&gt; exports;
if (mod != nullptr) {
exports = InitModule(env, mod, module);
} else if (!strcmp(*module_v, "constants")) {
exports = Object::New(env-&gt;isolate());
CHECK(exports-&gt;SetPrototype(env-&gt;context(),
Null(env-&gt;isolate())).FromJust());
DefineConstants(env-&gt;isolate(), exports);
} else if (!strcmp(*module_v, "natives")) { // NativeModule _source
exports = Object::New(env-&gt;isolate());
DefineJavaScript(env, exports);
} else {
return ThrowIfNoSuchModule(env, *module_v);
} args.GetReturnValue().Set(exports);

其作用就是根据传参来初始化指定的模块,当然也有比较特殊的两个分别是constantsnatives(后面再看),get_builtin_module调用的就是FindModule,还记得之前在Init过程中将模块都注册到的链表吗?FindModule就是遍历这个链表找到相应的模块:


struct node_module* mp;
for (mp = list; mp != nullptr; mp = mp-&gt;nm_link) {
if (strcmp(mp-&gt;nm_modname, name) == 0)
break;
}

InitModule就是调用之前注册模块定义的初始化函数,还以buffer看的话,就是执行node::Buffer::Initialize函数,打开着函数来看和平时写addon的方式一样,也会暴露一个对象出来供js调用;LoadEnvironment下面就是将process, GetBinding等作为传入传给上面生成好的函数并且利用v8来执行,来到了大家熟悉的领域,来看看loaders.js


const moduleLoadList = [];
ObjectDefineProperty(process, 'moduleLoadList', {
value: moduleLoadList,
configurable: true,
enumerable: true,
writable: false
});

定义了一个已经加载的Module的数组,也可以在node通过process.moduleLoadList来看看加载了多少的原生模块进来

process.binding


process.binding = function binding(module) {
module = String(module);
let mod = bindingObj[module];
if (typeof mod !== 'object') {
mod = bindingObj[module] = getBinding(module);
moduleLoadList.push(`Binding ${module}`);
}
return mod;
};

终于到了这个方法,翻看lib中的js文件,有着非常多的这种调用,这个函数就是对GetBinding做了一个js层面的封装,做的无非是查看一下这个模块是否已经加载完成了,是的话直接返回回去,不需要再次初始化了,所以利用prcoess.binding加载了对应的c++模块(可以执行一下process.binding('buffer'),然后再去node_buffer.cc中看看)继续向下看,会发现定义了一个class就是NativeModule,发现其有一个静态属性:

加载js


NativeModule._source = getBinding('natives');

返回到GetBinding函数,看到的是一个if分支就是这种情况:


exports = Object::New(env-&gt;isolate());
DefineJavaScript(env, exports);

来看看DefineJavaScript发生了什么样的事情,这个函数发现只能在头文件(node_javascript.h)里面找到,但是根本找不到具体的实现,这是个什么鬼???去翻一下node.gyp文件发现这个文件是用js2c.py这个文件生成的,去看一下这个python文件,可以发现许多的代码模板,每一个模板都是用Render返回的,data参数就是js文件的内容,最终会被转换为c++中的byte数组,同时定义了一个将其转换为字符串的方法,那么问题来了,这些文件都是那些呢?答案还是在node.gyp中,就是library_files数组,发现包含了lib下的所有的文件和一些dep下的js文件,DefineJavaScript这个文件做的就是将待执行的js代码注册下,所以NativeModule._source中存储的是一些待执行的js代码,来看一下NativeModule.require

NativeModule


const cached = NativeModule.getCached(id);
if (cached &amp;&amp; (cached.loaded || cached.loading)) {
return cached.exports;
}
moduleLoadList.push(`NativeModule ${id}`); const nativeModule = new NativeModule(id); nativeModule.cache();
nativeModule.compile(); return nativeModule.exports;

可以发现NativeModule也有着缓存的策略,require先把其放到_cache中再次require就不会像第一次那样执行这个模块,而是直接用缓存中执行好的,后面说的Module与其同理,看一下compile的实现:


let source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source); NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, process) {',
'\n});'
];

首先从_source中取出相应的模块,然后对这个模块进行包裹成一个函数,执行函数用的是什么呢?


const script = new ContextifyScript(
source, this.filename, 0, 0,
codeCache[this.id], false, undefined
); this.script = script;
const fn = script.runInThisContext(-1, true, false);
const requireFn = this.id.startsWith('internal/deps/') ?
NativeModule.requireForDeps :
NativeModule.require;
fn(this.exports, requireFn, this, process);

本质上就是调用了vm编译自妇产得到函数,然后给其传入了一些参数并执行,this.exports就是一个对象,require区分了一下是否加载node依赖的js文件,this也就是参数module,这也说明了两者的关系,exports就是module的一个属性,也解释了为什么exports.xx之后再指定module.exports = yy会将xx忽略掉,还记得LoadEnvironment吗?bootstrap/loaders.js执行完之后执行了bootstrap/node.js,可以说这个文件是node真正的入口,比如定义了global对象上的属性,比如console setTimeout等,由于篇幅有限,来挑一个最常用的场景,来看看这个是什么一回事:


else if (process.argv[1] &amp;&amp; process.argv[1] !== '-') {
const path = NativeModule.require('path');
process.argv[1] = path.resolve(process.argv[1]); const CJSModule = NativeModule.require('internal/modules/cjs/loader');
...
CJSModule.runMain();
}

这个过程就是熟悉的node index.js这个过程,可以看到的对于开发者自己的js来说,在node中对应的classModule,相信这个文件大家很多人都了解,与NativeModule相类似,不同的是,需要进行路径的解析和模块的查找等,来大致的看一下这个文件,先从上面调用的runMain来看:


if (experimentalModules) {
// ...
} else {
Module._load(process.argv[1], null, true);
}

Module

node中开启--experimental-modules可以加载es模块,也就是可以不用babel转义就可以使用import/export啦,这个不是重点,重点来看普通的commonnjs模块,process.argv[1]一般就是要执行的入口文件,下面看看Module._load


Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
// 查找文件具体位置
var filename = Module._resolveFilename(request, parent, isMain); // 存在缓存,则不需要再次执行
var cachedModule = Module._cache[filename];
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
} // 加载node原生模块,原生模块不需要缓存,因为NativeModule中也存在缓存
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
} // 加载并执行一个模块
var module = new Module(filename, parent); if (isMain) {
process.mainModule = module;
module.id = '.';
} Module._cache[filename] = module; // 调用load方法进行加载
tryModuleLoad(module, filename); return module.exports;
};

这里看每一个Module有一个parent的属性,假如a.js中引入了b.js,那么Module bparent就是Module a,利用resolveFilename可以得到文件具体的位置,这个过程而后调用load函数来加载文件,可以看到的是区分了几种类型,分别是.js .json .node,对应的.js是读文件然后执行,.json是直接读文件后JSON.parse一下,.node是调用dlopenModule.compileNativeModule.compile相类似都是想包裹一层成为函数,然后调用了vm编译得到这个函数,最后传入参数来执行,对于Module来说,包裹的代码如下:


Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];

执行完上述过程后,前期工作就已经做得比较充分了,再次回到最后一个Start函数来看,从代码中可以看到开始了nodeevent loop,这就是node的初始化过程,关于event loop需要对libuv有一定的了解,可以说node真正离不开的是libuv,具体这方面的东西,可以继续关注我后面的文章

总结

总结一下这个过程,以首次加载没有任何缓存的情况开看:require('fs'),先是调用了Module.require,而后发现为原生模块,于是调用NativeModule.require,从NativeModule._sourcelib/fs的内容拿出来包裹一下然后执行,这个文件第一行就可以看到process.binding,这个本质上是加载原生的c++模块,这个模块在初始化的时候将其注册到了一个链表中,加载的过程就是将其拿出来然后执行

以上内容如果有错误的地方,还请大佬指出,万分感激,另外一件重要的事情就是:我所在团队也在招人,如果有兴趣可以将简历发至zhoupeng.1996@bytedance.com

原文地址:https://segmentfault.com/a/1190000016318567

nodejs源码—初始化的更多相关文章

  1. 深入出不来nodejs源码-V8引擎初探

    原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...

  2. 深入出不来nodejs源码-timer模块(JS篇)

    鸽了好久,最近沉迷游戏,继续写点什么吧,也不知道有没有人看. 其实这个node的源码也不知道该怎么写了,很多模块涉及的东西比较深,JS和C++两头看,中间被工作耽搁回来就一脸懵逼了,所以还是挑一些简单 ...

  3. 深入出不来nodejs源码-events模块

    这一节内容超级简单,纯JS,就当给自己放个假了,V8引擎和node的C++代码看得有点脑阔疼. 学过DOM的应该都知道一个API,叫addeventlistener,即事件绑定.这个东西贯穿了整个JS ...

  4. 深入出不来nodejs源码-编译启动(1)

    整整弄了两天,踩了无数的坑,各种奇怪的error,最后终于编译成功了. 网上的教程基本上都过时了,或者是版本不对,都会报一些奇怪的错误,这里总结一下目前可行的流程. node版本:v10.1.0. 首 ...

  5. 深入出不来nodejs源码-从fs.stat方法来看node架构

    node的源码分析还挺多的,不过像我这样愣头完全平铺源码做解析的貌似还没有,所以开个先例,从一个API来了解node的调用链. 首先上一张整体的图,网上翻到的,自己懒得画: 这里的层次结构十分的清晰, ...

  6. 深入出不来nodejs源码-内置模块引入再探

    我发现每次细看源码都能发现我之前写的一些东西是错误的,去改掉吧,又很不协调,不改吧,看着又脑阔疼…… 所以,这一节再探,是对之前一些说法的纠正,另外再缝缝补补一些新的内容. 错误在哪呢?在之前的初探中 ...

  7. 深入出不来nodejs源码-流程总览

    花了差不多两周时间过了下primer C++5th,完成了<C++从入门到精通>.(手动滑稽) 这两天看了下node源码的一些入口方法,其实还是比较懵逼的,语法倒不是难点,主要是大量的宏造 ...

  8. Nodejs源码系列

    一直想着看Nodej源码,断断续续的折腾了一下,但总串不起来,太久不看又忘记.决心每天看一点,特地记录在这里,作为逼迫自己的动力. 2019/09/22 一.源码编译 之前在电脑上了下源码,源码目录截 ...

  9. Nodejs源码解析之module

    modulejs的导入 Require函数详解 module路径解析 module.js的导入 module.js是由node.js在Nodejs程序启动的时候导入的.module.js中使用的req ...

随机推荐

  1. linux读取yaml文件的某个属性值

    trigger=$(cat test.yaml | grep "trigger" | awk '{print $2}') 该条命令的意思是:读取test.yaml文件中的trigg ...

  2. SQL判断经纬度在矩形内

    1,将城市地图拆分等距拆分为矩形 数据结构如图: 2.查看高德JS API (点是否在多边形内)核心代码: a=[114.069564,22.545774]; b=[ [114.067595,22.5 ...

  3. HttpClient4.6的使用

    禁止转载,如需转载请联系本人 1)简介: HttpClient是apache的开源项目,弥补了Java自带的URLConnection功能不足,操作繁琐的缺点. 2)简单使用: a)get方式请求 / ...

  4. Maven的学习资料收集--(三)使用Maven构建Web项目

    新建Maven项目 File - New - Other 选择Maven Project 单击Next 保持默认即可单击Next 选择Archetype为 web app单击Next 输入一些必要信息 ...

  5. (转)JSP HTML JAVASCRIPT 中文乱码 解决方案 大全

    JSP HTML JAVASCRIPT 中文乱码 解决方案 大全 JSP的中文字符一直是各位初学者首先要解决的问题,下面进行了总结,也给出了解决办法.C4.1 HTML中文编码转换 在JSP文件中的静 ...

  6. Java学习笔记--字符串和文件IO

    1.Java中的字符串类和字符的表示 2.区分String,StringBuilder和StringBuffer 3.从命令行中给main方法传递参数 4.文件操作 1 Java中的字符串和字符 1. ...

  7. 使用Pycharm开发python下django框架项目生成的文件解释

    目录MyDjangoProject下表示工程的全局配置,分别为setttings.py.urls.py和wsgi.py,1.其中setttings.py包括了系统的数据库配置.应用配置和其他配置,2. ...

  8. Laravel事件监听器listener与事件订阅者Subscriber的区别

    其实就一句话: Each event can have multiple listeners, but a listener can't listen to more than a single ev ...

  9. defer=“defer”和async=“async”

    <script type="text/javascript" src="demo_defer.js" defer="defer"> ...

  10. nodejs的会话总结

    前言: http是一个无状态协议,所以客户端每次发出请求时,下一次请求就无法得知上一次请求所包含的状态数据,那么如何能把一个用户的状态数据关联起来?1.cookie 一开始,人们采用cookie这门技 ...