mongoose mysql_mongoose入门

mongoose入门

MongoDB是一个开源的NoSQL数据库,相比MySQL那样的关系型数据库,它更显得轻巧、灵活,非常适合在数据规模很大、事务性不强的场合下使用。同时它也是一个对象数据库,没有表、行等概念,也没有固定的模式和结构,所有的数据以文档的形式存储(文档,就是一个关联数组式的对象,它的内部由属性组成,一个属性对应的值可能是一个数、字符串、日期、数组,甚至是一个嵌套的文档。),数据格式就是JSON。

假定读者已经了解了mongdb和mysql的区别和为什么选用mongodb,继而介绍nodejs里的mongoose模块进行实战,具体分4步骤

mongoose是什么?

mongoose入门

mongoose概念

mongoose crud

Mongoose是什么?

Mongoose是MongoDB的一个对象模型工具,是基于node-mongodb-native开发的MongoDB nodejs驱动,可以在异步的环境下执行。同时它也是针对MongoDB操作的一个对象模型库,封装了MongoDB对文档的的一些增删改查等常用方法,让NodeJS操作Mongodb数据库变得更加灵活简单。

Mongoose,因为封装了对MongoDB对文档操作的常用处理方法,可以高效的操作mongodb,同时可以理解mongoose是一个简易版的orm ,提供了类似schema定义,hook、plugin、virtual、populate等机制,让NodeJS操作Mongodb数据库变得特别简单!

以往书中往往直接上例子,混合各种库和代码,容易让人晕,必须在例子中才能知道m是如何使用的,我一直认为这是不合理的,为什么我要掌握其他的知识才能学mongoose?

其实,它也仅仅是一个node模块而已。

mongoose入门

前面我们已经认识了Mongoose,也了解了MongoDB,回顾一下:MongoDB是一个对象数据库,是用来存储数据的;Mongoose是封装了MongoDB操作的一个对象模型库,是用来操作这些数据的。

好,下面我们就来进行操作数据的第一步吧。

准备

1、 安装mongoose

$ npm install --save mongoose

2、 引用mongoose

var mongoose = require("mongoose");

3、 使用"mongoose"连接数据库

var db = mongoose.connect("mongodb://user:pass@ip:port/database");

说明

user 是mongodb里用户名

pass 是mongodb里用户对应的密码

ip 是mongodb服务器可以访问ip地址,比如本地为127.0.0.1

port 是mongodb服务器可以访问端口,默认是27017

测试

执行下面代码检查默认数据库test,是否可以正常连接成功?

var mongoose = require("mongoose");

var db = mongoose.connect("mongodb://127.0.0.1:27017/db_helloworld");

db.connection.on("error", function (error) {

console.log("数据库连接失败:" + error);

});

db.connection.on("open", function () {

console.log("数据库连接成功");

});

执行代码

$ node book-source/db/helloworld/connect.js

数据库连接成功

当mongodb没有启动的时候会报错,当出现如下问题,请执行mhg启动mongodb即可

$ node book-source/db/helloworld/connect.js

数据库连接失败:MongoError: connect ECONNREFUSED 127.0.0.1:27017

最小demo

这里给出极简demo,用于讲解mongoose从连接数据库到对数据库进行操作完整过程,这样更容易让读者了解核心原理,代码如下:

// 1、引入`mongoose`模块

var mongoose = require('mongoose');

// 2、通过`mongoose.connect`连接mongodb数据库

mongoose.connect('mongodb://127.0.0.1/db_helloworld');

// 3、通过`mongoose.model`定义模型(model)

var Cat = mongoose.model('Cat', { name: String });

// 4、通过`new`关键字实例化Cat模型,参数是`{ name: 'Zildjian' }`,创建kitty对象

var kitty = new Cat({ name: 'Zildjian' });

// 5、执行`kitty.save`来保存到数据库

kitty.save(function (err) {

if (err) {

console.log('save error:' + err);

}

console.log('save sucess');

});

核心步骤说明

定义模型(model)

通过new关键字实例化Cat模型,创建kitty对象

执行kitty.save来保存到数据库

这个其实就是mongoose最常见的用法,首先约定schema,即在模型model定义的时候指定字段和字段类型,避免乱用schema-free问题。之后对实例化模型创建的对象进行操作,完成我们常见的增删改查功能。

模型(model)定义即定义对象,对象操作即对数据库进行操作

执行如下

$ node book-source/db/helloworld/helloworld.js

如无错误日志,即代表数据保存成功。此时打开robo mongodb客户端查看一下具体数据是否保存成功。

创建robo客户端连接

[图片上传失败...(image-a2a818-1519355504563)]

查看cat数据

[图片上传失败...(image-a49fdc-1519355504563)]

如果有{ name: 'Zildjian' }记录的,即为正常。

实例

连接数据库信息放到独立文件里

模型定义放到独立文件

在具体调用的文件里,使用模型定义

连接数据库

看一下实际代码,db/mini/connect.js

var mongoose = require("mongoose");

var db = mongoose.connect("mongodb://127.0.0.1:27017/db_helloworld");

db.connection.on("error", function (error) {

console.log("数据库连接失败:" + error);

});

db.connection.on("open", function () {

console.log("数据库连接成功");

});

一般项目里,所有模型都共用一个数据库连接信息,所以把连接数据库的代码抽取到connect.js里,然后在对应的模型里会app入口引用即可。

模型定义

看一下实际模型定义代码,db/mini/user.js

var mongoose = require('mongoose');

// 定义Schema

UserSchema = new mongoose.Schema({

username: {// 真实姓名

type: String,

required: true

},

password: { // 密码

type: String,

required: true

}

});

// 定义Model

var UserModel = mongoose.model('User', UserSchema);

// 暴露接口

module.exports = UserModel;

这是MVC里Model层最长见的代码,没有连接信息,也没有其他额外不相干代码,当你看到user.js你就能理解它在数据库里对应的表结构,以及字段的类型、约束等信息,一般来说,代码写的越干净,可读性会更好一些。

这里定义的User模型里只有 用户名 和 密码 2个字段,它们都必须有值的,也就是说当你创建用户的时候,没有密码或者没有用户名你是无法创建成功的。

测试代码

看一下实际代码db/mini/user.js

// 1、引入`mongoose connect`

require('./connect');

// 2、引入`User` Model

var User = require('./user');

// 3、定义`user` Entity

var user = new User({

username: 'i5ting',

password: '0123456789'

});

// 4、对数据库进行操作

user.save(function(err, doc){

if (err) {

console.log('save error:' + err);

}

console.log('save sucess \n' + doc);

})

核心步骤

引入数据库连接,保证mongodb已经连接成功

引入模型(model)定义文件,即文档(表)结构定义

实例化UserModel,创建user实体

最后通过user实体对数据库进行操作,完成用户注册功能。

这是项目里数据访问层的代码,它真实的对数据库进行操作,所以它一般会出现在controller或service层。

执行测试

$ node db/mini/test.js

数据库连接成功

save sucess

{ _id: 57341fc54d97ee0249082a1d,

password: '0123456789',

username: 'i5ting',

__v: 0 }

概念

结合上面的实例来讲4个核心概念,以便于理解

ORM 对象关系映射

Schema

Model 模型

Entity 实体

对象关系映射

对象关系映射(英语:Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。从效果上说,它其实是创建了一个可在编程语言里使用的“虚拟对象数据库”。如今已有很多免费和收费的ORM产品,而有些程序员更倾向于创建自己的ORM工具。

面向对象是从软件工程基本原则(如耦合、聚合、封装)的基础上发展起来的,而关系数据库则是从数学理论发展而来的,两套理论存在显著的区别。为了解决这个不匹配的现象,对象关系映射技术应运而生。

对象关系映射(Object-Relational Mapping)提供了概念性的、易于理解的模型化数据的方法。

ORM方法论基于三个核心原则:

简单:以最基本的形式建模数据。

传达性:数据库结构被任何人都能理解的语言文档化。

精确性:基于数据模型创建正确标准化的结构。

典型地,建模者通过收集来自那些熟悉应用程序但不熟练的数据建模者的人的信息开发信息模型。建模者必须能够用非技术企业专家可以理解的术语在概念层次上与数据结构进行通讯。建模者也必须能以简单的单元分析信息,对样本数据进行处理。ORM专门被设计为改进这种联系。

让我们从O/R开始。字母O起源于 对象(OBJECT),而R则来自于 关系(RELATIONAL)。几乎所有的程序里面,都存在对象和关系数据库。在业务逻辑层和用户界面层中,我们是面向对象的。当对象信息发生变化的时候,我们需要把对象的信息保存在关系数据库中。

最简单的理解:

ORM是让用语言中的对象来操作数据库,至于如何实现就是orm工具实现的,可以理解mongoose是orm工具。

mongoose包括以下四部分:

一个对持久类对象进行CRUD操作的API,可以理解为实体Entity上的方法

一个语言或API用来规定与类和类属性相关的查询,比如Population

一个规定MAPPING METADATA的工具,可以理解为Schema定义

一种技术可以让ORM的实现各种db操作的封装

Schema

Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.

Schema是一种以文件形式存储的数据库模型骨架,无法直接通往数据库端,也就是说它不具备对数据库的操作能力,仅仅只是定义数据库模型在程序片段中的一种表现,可以说是数据属性模型(传统意义的表结构),又或着是“集合”的模型骨架。

最简单的理解:

Schema是对文档(表)结构的定义

那如何去定义一个Schema呢,请看示例:

// 定义Schema

UserSchema = new mongoose.Schema({

username: {// 真实姓名

type: String,

required: true

},

password: { // 密码

type: String,

required: true

}

});

基本属性类型有:字符串、日期型、数值型、布尔型(Boolean)、null、数组、内嵌文档等,当然它还有更丰富的对字段进行校验约束的功能。

模型(Model)

Models are fancy constructors compiled from our Schema definitions. Instances of these models represent documents which can be saved and retrieved from our database. All document creation and retrieval from the database is handled by these models.

模型(Model)是由Schema构造生成的模型,除了Schema定义的数据库骨架以外,还具有数据库操作的行为,类似于管理数据库属性、行为的类。

如何通过Schema来创建Model呢,如下示例:

var db = mongoose.connect("mongodb://127.0.0.1:27017/test");

// 创建Model

var TestModel = db.model("test1", TestSchema);

// 定义Model

var UserModel = mongoose.model('User', UserSchema);

User是模型名称,它对应到mongodb里就是数据库中的集合名称,默认会转成复数,变为'users',当我们对其添加数据时如果users已经存在,则会保存到其目录下,如果未存在,则会创建users集合,然后在保存数据。

拥有了Model,我们也就拥有了操作数据库的金钥匙,在后面的内容中,我们就会学习使用Model来进行增删改查的具体操作,所以,一定要熟悉他的创建格式哟!

如果你想对某个集合有所作为,那就交给Model模型来处理吧,创建一个Model模型,我们需要指定:1.集合名称,2.集合的Schema结构对象,满足这两个条件,我们就会拥有一个操作数据库的金钥匙。

实体(Entity)

Mongoose documents represent a one-to-one mapping to documents as stored in MongoDB. Each document is an instance of its Model.

实体(Entity)是由Model创建的实体,使用save方法保存数据,Model和Entity都有能影响数据库的操作,但Model比Entity更具操作性。

使用Model创建Entity,如下示例:

var user = new User({

username: 'i5ting',

password: '0123456789'

});

console.log(user.username); // i5ting

console.log(user.password); //0123456789

创建成功之后,Schema属性就变成了Model和Entity的公共属性了。

总结

Schema是骨架,模型(model)是根据Schema创建的模板,也就是说Schema和Model是定义部分,而实体Entity是模型实例化后创建的对象,它才是真正对数据库进行操作的。

所以我们会把定义部分(Schema + model)和实体操作部分(Entity)分开,定义是不变的,而实体是对数据库进行操作,操作类是术语可变的,所以在mvc分层的时候model实际放的是定义部分,而在controller里使用的是实体操作部分的。

基于前面的内容,接下来我们就开始学习对数据的具体操作了,下面是关于一些基础数据的定义,相信对于你来说已经不陌生了,请在仔细温习一遍吧!

CRUD(增删改查)

CRUD为数据库的最常见的4种基本操作,即增加(Create)、读取(Retrieve)(重新得到数据)、更新(Update)和删除(Delete)几个单词的首字母简写。主要被用在描述软件系统中数据库或者持久层的基本操作功能。

mongoose提供如下的crud方法

save

find | findOne

update

remove

下面我们使用user模型为例,给出具体例子

增加(Create)

文档

Model#save(product,)

@description Saves this document.

Parameters:

- product, Number)} [fn] optional callback

Returns:

Promise

具体代码

const user = new User({

username: 'i5ting',

password: '0123456789'

});

user.save((err, u) => {

t.false(err);

t.is(u.username, 'i5ting');

});

读取(Retrieve)

find:根据条件查询,返回的是数组

文档

Model.find(conditions, [fields], [options], [callback])

Finds documents

Parameters:

- conditions

- [fields] optional fields to select

- [options] optional

- [callback]

Returns:

代码

User.find({}, (err, docs) => {

t.false(err);

t.is(docs.length, 1);

t.is(docs[0].username, 'i5ting');

});

findOne:根据条件查询,返回的是一条数据对象

文档

Model.findOne([conditions], [fields], [options], [callback])

Finds one document.

Parameters:

- [conditions]

- [fields] optional fields to select

- [options] optional

- [callback]

Returns:

代码

User.findOne({username: 'i5ting'}, (err, doc) => {

t.false(err);

t.is(doc.length, 1);

t.is(doc.username, 'i5ting');

});

更新(Update)

findByIdAndUpdate:根据ID查找并更新

文档说明如下

Model.findByIdAndUpdate(id, [update], [options], [callback])

Issues a mongodb findAndModify update command by a documents id.

show code

Parameters:

- id an ObjectId or string that can be cast to one.

- [update]

- [options]

- [callback]

Returns:

具体代码

User.findByIdAndUpdate(u._id, {

username: 'sang',

}, (err, user) => {

t.false(err);

t.is(user.username, 'sang');

});

findOneAndUpdate:根据查询条件查找并更新

Model.findOneAndUpdate([conditions], [update], [options], [callback])

Issues a mongodb findAndModify update command.

Parameters:

- [conditions]

- [update]

- [options]

- [callback]

Returns:

具体代码

User.findOneAndUpdate({

username: 'i5ting for update 2',

}, {

username: 'sang',

}, (err, user) => {

t.false(err);

t.is(user.username, 'sang');

});

删除(Delete)

文档

Model.remove(conditions, [callback])

Removes documents from the collection.

Parameters:

- conditions

- [callback]

Returns:

Promise

代码

User.remove({username: 'i5ting for delete'}, (err, doc) => {

t.false(err);

t.is(doc.result.ok, 1);

t.is(doc.result.n, 1);

});

执行测试

$ cd db

$ npm test

> koa-db@1.0.0 test /Users/sang/workspace/17koa/book-source/db

> ava crud -v

数据库连接成功

✔ #save()

✔ #find() return array

✔ #findById() return array

✔ #findOne() return user obj

✔ #remove()

✔ #findByIdAndUpdate()

✔ #findOneAndUpdate()

6 tests passed

调试模式

调试模式是mongoose提供的一个非常实用的功能,用于查看mongoose模块对mongodb操作的日志,一般开发时会打开此功能,以便更好的了解和优化对mongodb的操作。

打开调试的核心代码是设置 debug 变量值为 true 即可

var mongoose = require("mongoose");

// 核心代码,是否开启测试

mongoose.set('debug', true);

var db = mongoose.connect("mongodb://127.0.0.1:27017/db_helloworld");

db.connection.on("error", function (error) {

console.log("数据库连接失败:" + error);

});

db.connection.on("open", function () {

console.log("数据库连接成功");

});

执行测试如下

$ cd db

$ npm test

> koa-db@1.0.0 test /Users/sang/workspace/17koa/book-source/db

> ava crud -v

数据库连接成功

Mongoose: users.remove({}) {}

✔ #save()

✔ #find() return array

✔ #findById() return array

✔ #findOne() return user obj

✔ #remove()

✔ #findByIdAndUpdate()

✔ #findOneAndUpdate()

Mongoose: users.insert({ username: 'i5ting', password: '0123456789', _id: ObjectId("5734490640caa6d36906b8d4"), __v: 0 })

Mongoose: users.find({}) { fields: undefined }

Mongoose: users.findOne({ username: 'i5ting' }) { fields: undefined }

Mongoose: users.insert({ username: 'i5ting for delete', password: '0123456789', _id: ObjectId("5734490640caa6d36906b8d5"), __v: 0 })

Mongoose: users.insert({ username: 'i5ting for update 1', password: '0123456789', _id: ObjectId("5734490640caa6d36906b8d6"), __v: 0 })

Mongoose: users.insert({ username: 'i5ting for update 2', password: '0123456789', _id: ObjectId("5734490640caa6d36906b8d7"), __v: 0 })

Mongoose: users.remove({ username: 'i5ting for delete' }) {}

Mongoose: users.findAndModify({ _id: ObjectId("5734490640caa6d36906b8d6") }) [] { '$set': { username: 'sang' } } { new: false, upsert: false }

Mongoose: users.findAndModify({ username: 'i5ting for update 2' }) [] { '$set': { username: 'sang' } } { new: false, upsert: false }

6 tests passed

总结

从mongoose是什么,到如何使用,以及核心概念(orm以及schema、model、entity),最后给出CRUD操作,希望读者能够认真体会orm和具体分层含义。

本节以最简单的user,结合ava完成单元测试,我们测试数据库代码并不一定要在koa或其他框架内部,最小化问题,mongoose它只是一个node模块,这样对于我们理解它是比较好的方式。接下来我们会讲解更多高级mongoose技巧,它们的基础都是本节内容,所以本节必须掌握,是本章重点。

其他

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/572523.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php 验证码需开启gd2

转载于:https://www.cnblogs.com/zhangj391/p/6794555.html

pcm 降采样_Android_android downsample降低音频采样频率代码,使用Android AudioRecord 录制PCM文 - phpStudy...

android downsample降低音频采样频率代码使用Android AudioRecord 录制PCM文件,android SDK保证在所有设备上都支持的采样频率只有44100HZ,所以如果想得到其他采样频率的PCM数据,有几种方式:1.在设备上尝试可用的采样频率&#xf…

C++中没有定义类的引用。

在有时候由于类太大。须要在类在后面定义; 比如: class Y{void f(X); }; class X{ //一些成员数据和函数};//error由于c要求不论什么一个变量在引用之前必须声明。。在上述定义中我们能够调换两者的顺序来实现。 可是假设形成了循环? class X…

python中不能使用下标运算的有哪些_Python中最常见的10个问题(列表)

列表是Python中使用最多的一种数据结果,如何高效操作列表是提高代码运行效率的关键,这篇文章列出了10个常用的列表操作,希望对你有帮助。注意:很多人学Python过程中会遇到各种烦恼问题,没有人帮答疑。为此小编建了个Py…

Spring:与Redis的集成

一个月没写过博客了,一直想记录一下之前学习的Redis的有关知识,但是因为四月太过于慵懒和忙碌,所以一直没有什么机会,今天就来讲讲,如何使用Spring当中的Spring-data-redis去与Redis这个Nosql数据库集成吧。 首先先简单…

python中如何将字典直接变成二维数组_python基础知识(列表、字典、二维数组)...

记得改参数!!!(1)简述列表(list)结构的特点。List(列表)List的元素以线性方式存储,可以存放重复对象,List主要有以下两个实现类:ArrayList : 长度可变的数组,可以对元素进行随机的访问&#xff…

内置对象和自定义对象的原型链

内置对象的原型链 新增属性 Object.prototype.mytest function() {alert("123");}var o new Object();o.mytest();重写属性 Object.prototype.toString function(){alert("破话之王");}o.toString();转载于:https://www.cnblogs.com/Abner5/p/6798630.h…

mysql将不同行数结果合并成多列_将多行合并到mysql中的一行和多列

我正在MYSQL工作,需要将用户数据提取到一个视图中.我将使用电子邮件客户端中的数据,因此我无法在应用程序层中执行此操作.问题是用户的每个数据字段都包含在单独的行中(这是Wordpress如何设置数据结构).例如,wp_usermeta为每个用户提供了多行数据,如下所示:user_id …

一些JavaScript基本函数

1.document.write(”");为 输出语句 2.JS中的注释为// 3.传统的HTML文档顺序是:document->html->(head,body) 4.一个浏览器窗口中的DOM顺序是:window->(navigator,screen,history,location,document) 5.得到表单中元素的名称和值:document.getElementById(”表单…

mysql动态代理_超全MyBatis动态代理详解(绝对干货)

前言假如有人问你这么几个问题,看能不能答上来Mybatis Mapper 接口没有实现类,怎么实现的动态代理JDK 动态代理为什么不能对类进行代理(充话费送的问题)抽象类可不可以进行 JDK 动态代理(附加问题)答不上来的铁汁,证明 Proxy、Mybatis 源码还…

Linux学习之SAMBA共享(无密码)

一、服务器配置 安装:yum install -y samba 更改要共享的path权限:chmod 777 /mnt/samba/ 先备份smb.conf配置文件:cp /etc/samba/smb.conf /etc/samba/smb.conf.bak 更改配置:vim /etc/samba/smb.conf security sha…

getline()函数

本文转自:http://blog.sina.com.cn/s/blog_60263c1c0101ck25.html学习C的同学可能都会遇到一个getline()函数,譬如在Cpremer中,标准string类型第二小节就是“用getline读取整行文本”。书上给的程序如下:int main(){string line:w…

廖雪峰讲python高阶函数求导公式_高阶函数 - 廖雪峰 Python 2.7 中文教程

高阶函数英文叫Higher-order function。什么是高阶函数?我们以实际代码为例子,一步一步深入概念。变量可以指向函数以Python内置的求绝对值的函数abs()为例,调用该函数用以下代码:>>> abs(-10)10但是,如果只写…

.ai域名注册已经极具投资价值进入火爆期

最近G.ai以六位数的天价被国内域名收藏家收入囊中,间接说明了.ai域名的价值不断攀升,自从2016年AlphaGo胜利以来,人工智能几乎成为人人谈资,而由于.com域名被挖掘待尽,一些聪明的人工智能创业者,避开了com域…

软件工程概论---典型用户和用户场景描述

二手书交易平台典型用户——在校学生 名字:小张 年龄:19 收入:学生 代表的用户在市场上的比例和重要性:主要代表广大的大学生,大学生应该是我们产品的最主要用户。 使用这个软件的典型场景:大学生苦于自己的…

python pygame鼠标点击_Python中pygame的mouse鼠标事件用法实例

本文实例讲述了Python中pygame的mouse鼠标事件用法。分享给大家供大家参考,具体如下:pygame.mouse提供了一些方法获取鼠标设备当前的状态pygame.mouse.get_pressed - get the state of the mouse buttons get the state of the mouse buttonspygame.mous…

题目1089:数字反转(简单数字转换)

题目链接:http://ac.jobdu.com/problem.php?pid1089 详解链接:https://github.com/zpfbuaa/JobduInCPlusPlus 参考代码: // // 1089 数字反转.cpp // Jobdu // // Created by PengFei_Zheng on 04/05/2017. // Copyright © 2017 Pe…

SpringSecurity自定义登陆页面和跳转页面

如果我们不用form-login说明登陆界面&#xff0c;springsecurity框架将自动为我们生成登陆界面 现在我们不想用自动生成的登陆界面了&#xff0c;而想使用自定义的漂亮的登陆界面 则需要使用<security:form-login/>标签 login-page:指定登陆页面 login-process-url:表单提…

java 建模_Java设计模式菜鸟系列(两)建模与观察者模式的实现

观察者(Observer)模式定义&#xff1a;在对象之间定义了一对多的依赖关系&#xff0c;这样一来。当一个对象改变状态时&#xff0c;依赖它的对象都会收到通知并自己主动跟新。Java已经提供了对观察者Observer模式的默认实现&#xff0c;Java对观察者模式的支持主要体如今Observ…

php多条件查询

需要查询的表格 代码&#xff1a; <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns"http://www.w3.org/1999/xhtml"> <head> <…