Express 文档(使用中间件)

使用中间件Express是一个路由和中间件Web框架,其本身的功能非常小:Express应用程序本质上是一系列中间件函数调用。中间件函数是可以访问请求对象(req)、响应对象(res)以及应用程序请求—响应周期中的下一个中间件函数的函数,下一个中间件函数通常由名为next的变量表示。中间件函数可以执行以下任务:执行任何代码。更改请求和响应对象。结束请求—响应周期。调用堆栈中的下一个中间件函数。如果当前的中间件函数没有结束请求—响应周期,它必须调用next()将控制权传递给下一个中间件函数,否则,请求将被挂起。Express应用程序可以使用以下类型的中间件:应用程序级中间件路由器级中间件错误处理中间件内置中间件第三方中间件你可以使用可选的装载路径加载应用程序级和路由器级中间件,你还可以将一系列中间件函数加载在一起,从而在装载点创建中间件系统的子堆栈。应用程序级中间件使用app.use()和app.METHOD()函数将应用程序级中间件绑定到app对象的实例,其中METHOD是中间件函数处理的请求的小写HTTP方法(例如GET,PUT或POST)。此示例显示了没有装载路径的中间件函数,每次应用程序收到请求时都会执行该函数。var app = express()app.use(function (req, res, next) { console.log(‘Time:’, Date.now()) next()})此示例显示了安装在/user/:id路径上的中间件函数,对/user/:id路径上的任何类型的HTTP请求执行该函数。app.use(’/user/:id’, function (req, res, next) { console.log(‘Request Type:’, req.method) next()})此示例显示了路由及其处理函数(中间件系统),该函数处理对/user/:id路径的GET请求。app.get(’/user/:id’, function (req, res, next) { res.send(‘USER’)})下面是一个使用挂载路径在挂载点加载一系列中间件函数的示例,它说明了一个中间件子堆栈,它将任何类型的HTTP请求的请求信息打印到/user/:id路径。app.use(’/user/:id’, function (req, res, next) { console.log(‘Request URL:’, req.originalUrl) next()}, function (req, res, next) { console.log(‘Request Type:’, req.method) next()})路由处理程序使你可以为路径定义多个路由,下面的示例为/user/:id路径定义了两个GET请求路由,第二个路由不会引起任何问题,但它永远不会被调用,因为第一个路由结束了请求—响应周期。此示例显示了一个中间件子堆栈,它处理对/user/:id路径的GET请求。app.get(’/user/:id’, function (req, res, next) { console.log(‘ID:’, req.params.id) next()}, function (req, res, next) { res.send(‘User Info’)})// handler for the /user/:id path, which prints the user IDapp.get(’/user/:id’, function (req, res, next) { res.end(req.params.id)})要从路由器中间件堆栈跳过其余的中间件函数,请调用next(‘route’)将控制权传递给下一个路由,注意:next(‘route’)仅适用于使用app.METHOD()或router.METHOD()函数加载的中间件函数。此示例显示了一个中间件子堆栈,它处理对/user/:id路径的GET请求。app.get(’/user/:id’, function (req, res, next) { // if the user ID is 0, skip to the next route if (req.params.id === ‘0’) next(‘route’) // otherwise pass the control to the next middleware function in this stack else next()}, function (req, res, next) { // send a regular response res.send(‘regular’)})// handler for the /user/:id path, which sends a special responseapp.get(’/user/:id’, function (req, res, next) { res.send(‘special’)})路由器级中间件路由器级中间件的工作方式与应用程序级中间件的工作方式相同,只是它绑定到express.Router()的实例。var router = express.Router()使用router.use()和router.METHOD()函数加载路由器级中间件。以下示例代码通过使用路由器级中间件复制上面显示的应用程序级中间件的中间件系统:var app = express()var router = express.Router()// a middleware function with no mount path. This code is executed for every request to the routerrouter.use(function (req, res, next) { console.log(‘Time:’, Date.now()) next()})// a middleware sub-stack shows request info for any type of HTTP request to the /user/:id pathrouter.use(’/user/:id’, function (req, res, next) { console.log(‘Request URL:’, req.originalUrl) next()}, function (req, res, next) { console.log(‘Request Type:’, req.method) next()})// a middleware sub-stack that handles GET requests to the /user/:id pathrouter.get(’/user/:id’, function (req, res, next) { // if the user ID is 0, skip to the next router if (req.params.id === ‘0’) next(‘route’) // otherwise pass control to the next middleware function in this stack else next()}, function (req, res, next) { // render a regular page res.render(‘regular’)})// handler for the /user/:id path, which renders a special pagerouter.get(’/user/:id’, function (req, res, next) { console.log(req.params.id) res.render(‘special’)})// mount the router on the appapp.use(’/’, router)要跳过其余路由器中间件函数,请调用next(‘router’)将控制权交还出路由器实例。此示例显示了一个中间件子堆栈,它处理对/admin路径的GET请求。var app = express()var router = express.Router()// predicate the router with a check and bail out when neededrouter.use(function (req, res, next) { if (!req.headers[‘x-auth’]) return next(‘router’) next()})router.get(’/’, function (req, res) { res.send(‘hello, user!’)})// use the router and 401 anything falling throughapp.use(’/admin’, router, function (req, res) { res.sendStatus(401)})错误处理中间件错误处理中间件总是需要四个参数,你必须提供四个参数以将其标识为错误处理中间件函数,即使你不需要使用next对象,也必须指定它以维护签名,否则,next对象将被解释为常规中间件,并且将无法处理错误。以与其他中间件函数相同的方式定义错误处理中间件函数,除了四个参数而不是三个,特别是签名(err, req, res, next):app.use(function (err, req, res, next) { console.error(err.stack) res.status(500).send(‘Something broke!’)})有关错误处理中间件的详细信息,请参阅:错误处理。内置中间件从版本4.x开始,Express不再依赖于Connect,之前包含在Express中的中间件函数现在位于不同的模块中,查看中间件函数列表。Express具有以下内置中间件函数:express.static提供静态资源,如HTML文件、图像等。express.json使用JSON的有效负载解析传入的请求,注意:适用于Express 4.16.0+。express.urlencoded使用URL编码的有效负载解析传入的请求,注意:适用于Express 4.16.0+。第三方中间件使用第三方中间件为Express应用程序添加功能。安装Node.js模块以获得所需的功能,然后在应用程序级别或路由器级别将其加载到你的应用程序中。以下示例说明了安装和加载cookie解析中间件函数cookie-parser。$ npm install cookie-parservar express = require(’express’)var app = express()var cookieParser = require(‘cookie-parser’)// load the cookie-parsing middlewareapp.use(cookieParser())有关Express常用的第三方中间件函数的部分列表,请参阅:第三方中间件。上一篇:编写中间件下一篇:使用模板引擎 ...

December 27, 2018 · 2 min · jiezi

Express 文档(使用模板引擎)

使用模板引擎模板引擎使你可以在应用程序中使用静态模板文件,在运行时,模板引擎用实际值替换模板文件中的变量,并将模板转换为发送到客户端的HTML文件,这种方法可以更轻松地设计HTML页面。一些与Express一起使用的流行模板引擎是Pug、Mustache和EJS,Express应用程序生成器使用Jade作为其默认值,但它也支持其他几个。有关可与Express一起使用的模板引擎列表,请参阅模板引擎(Express wiki),另请参阅比较JavaScript模板引擎:Jade、Moustache、Dust等。注意:Jade已更名为Pug,你可以继续在你的应用中使用Jade,它可以正常工作,但是,如果你想要模板引擎的最新更新,则必须在应用程序中将Jade替换为Pug。要渲染模板文件,请设置以下应用程序设置属性,在由生成器创建的默认应用程序app.js中设置:views,模板文件所在的目录,例如:app.set(‘views’, ‘./views’),默认为应用程序根目录中的views目录。view engine,使用的模板引擎,例如,要使用Pug模板引擎:app.set(‘view engine’, ‘pug’)。然后安装相应的模板引擎npm包,例如安装Pug:$ npm install pug –save兼容Express的模板引擎(如Jade和Pug)导出名为__express(filePath, options, callback)的函数,该函数由res.render()函数调用以渲染模板代码。某些模板引擎不遵循此约定,Consolidate.js库遵循此约定,映射所有流行的Node.js模板引擎,因此可以在Express中无缝工作。设置视图引擎后,你不必在应用程序中指定引擎或加载模板引擎模块,Express在内部加载模块,如下所示(对于上面的示例)。app.set(‘view engine’, ‘pug’)在views目录中创建一个名为index.pug的Pug模板文件,其中包含以下内容:html head title= title body h1= message然后创建一个路由来渲染index.pug文件,如果未设置view engine属性,则必须指定视图文件的扩展名,否则,你可以省略它。app.get(’/’, function (req, res) { res.render(‘index’, { title: ‘Hey’, message: ‘Hello there!’ })})当你向主页发出请求时,index.pug文件将渲染为HTML。注意:视图引擎缓存不会缓存模板输出的内容,只缓存底层模板本身,即使缓存已打开,仍会每个请求重新渲染视图。要了解有关模板引擎如何在Express中工作的更多信息,请参阅:“为Express开发模板引擎”。上一篇:使用中间件

December 27, 2018 · 1 min · jiezi

Express 文档(编写中间件)

编写中间件中间件函数是可以访问请求对象(req)、响应对象(res)以及应用程序请求—响应周期中的next函数的函数,next函数是Express路由器中的一个函数,当被调用时,它会在当前中间件之后执行中间件。中间件函数可以执行以下任务:执行任何代码。更改请求和响应对象。结束请求—响应周期。调用堆栈中的下一个中间件。如果当前的中间件函数没有结束请求—响应周期,它必须调用next()将控制权传递给下一个中间件函数,否则,请求将被挂起。下图显示了中间件函数调用的元素:示例以下是一个简单的“Hello World” Express应用程序示例,本文的其余部分将为应用程序定义和添加两个中间件函数:一个名为myLogger,用于打印简单的日志消息,另一个名为requestTime,用于显示HTTP请求的时间戳。var express = require(’express’)var app = express()app.get(’/’, function (req, res) { res.send(‘Hello World!’)})app.listen(3000)中间件函数myLogger这是一个名为“myLogger”的中间件函数的简单示例,当对应用程序的请求通过时,此函数只打印“LOGGED”,中间件函数被分配给名为myLogger的变量。var myLogger = function (req, res, next) { console.log(‘LOGGED’) next()}注意上面的调用next(),调用此函数会调用应用程序中的下一个中间件函数,next()函数不是Node.js或Express API的一部分,而是传递给中间件函数的第三个参数。next()函数可以命名为任何,但按照惯例,它总是被命名为“next”,为避免混淆,请始终使用此约定。要加载中间件函数,请调用app.use(),指定中间件函数,例如,以下代码在到根路径(/)的路由之前加载myLogger中间件函数。var express = require(’express’)var app = express()var myLogger = function (req, res, next) { console.log(‘LOGGED’) next()}app.use(myLogger)app.get(’/’, function (req, res) { res.send(‘Hello World!’)})app.listen(3000)每次应用程序收到请求时,它都会向终端输出消息“LOGGED”。中间件加载的顺序很重要:首先加载的中间件函数也会先执行。如果myLogger在到达根路径的路由之后加载,则请求永远不会到达它,并且应用程序不会打印“LOGGED”,因为根路径的路由处理程序会终止请求—响应周期。中间件函数myLogger只是打印一条消息,然后通过调用next()函数将请求传递给堆栈中的下一个中间件函数。中间件函数requestTime接下来,我们将创建一个名为“requestTime”的中间件函数,并将一个名为requestTime的属性添加到请求对象中。var requestTime = function (req, res, next) { req.requestTime = Date.now() next()}该应用程序现在使用requestTime中间件函数,此外,根路径路由的回调函数使用中间件函数添加到req(请求对象)的属性。此外,根路径路由的回调函数使用中间件函数添加到req(请求对象)的属性。var express = require(’express’)var app = express()var requestTime = function (req, res, next) { req.requestTime = Date.now() next()}app.use(requestTime)app.get(’/’, function (req, res) { var responseText = ‘Hello World!<br>’ responseText += ‘<small>Requested at: ’ + req.requestTime + ‘</small>’ res.send(responseText)})app.listen(3000)当你向应用程序的根目录发出请求时,应用程序现在会在浏览器中显示你的请求的时间戳。因为你可以访问请求对象、响应对象、堆栈中的下一个中间件函数以及整个Node.js API,所以中间件函数的可能性是无穷无尽的。有关Express中间件的更多信息,请参阅:使用Express中间件。可配置的中间件如果你需要中间件可配置,请导出一个接受选项对象或其他参数的函数,然后根据输入参数返回中间件实现。文件:my-middleware.jsmodule.exports = function(options) { return function(req, res, next) { // Implement the middleware function based on the options object next() }}现在可以使用中间件,如下所示。var mw = require(’./my-middleware.js’)app.use(mw({ option1: ‘1’, option2: ‘2’ }))有关可配置中间件的示例,请参阅cookie-session和compression。上一篇:路由 ...

December 27, 2018 · 1 min · jiezi

Express 文档(路由)

路由路由是指应用程序的端点(URI)如何响应客户端请求,有关路由的介绍,请参阅路由基础。使用与HTTP方法相对应的Express app对象的方法定义路由,例如,app.get()用于处理GET请求,app.post()用于处理POST请求,有关完整列表,请参阅app.METHOD。你还可以使用app.all()来处理所有HTTP方法,并使用app.use()将中间件指定为回调函数(有关详细信息,请参阅使用中间件)。这些路由方法指定当应用程序收到对指定路由(端点)和HTTP方法的请求时调用的回调函数(有时称为“处理函数”),换句话说,应用程序“监听”与指定路由和方法匹配的请求,并且当它检测到匹配时,它调用指定的回调函数。实际上,路由方法可以有多个回调函数作为参数,使用多个回调函数时,重要的是提供next作为回调函数的参数,然后在函数体内调用next()以将控制权交给下一个回调。以下代码是一个非常基础的路由示例。var express = require(’express’)var app = express()// respond with “hello world” when a GET request is made to the homepageapp.get(’/’, function (req, res) { res.send(‘hello world’)})路由方法路由方法是从其中一个HTTP方法派生的,并附加到express类的实例。以下代码是为应用程序根目录的GET和POST方法定义的路由示例。// GET method routeapp.get(’/’, function (req, res) { res.send(‘GET request to the homepage’)})// POST method routeapp.post(’/’, function (req, res) { res.send(‘POST request to the homepage’)})Express支持与所有HTTP请求方法相对应的方法:get、post等,有关完整列表,请参阅app.METHOD。有一种特殊的路由方法app.all(),用于在路径上为所有HTTP请求方法加载中间件函数,例如,无论是使用GET、POST、PUT、DELETE还是http模块支持的任何其他HTTP请求方法,都会对路由“/secret”的请求执行以下处理程序。app.all(’/secret’, function (req, res, next) { console.log(‘Accessing the secret section …’) next() // pass control to the next handler})路由路径路由路径与请求方法结合,定义可以发出请求的端点,路由路径可以是字符串、字符串模式或正则表达式。字符?、+、和()是它们的正则表达式对应物的子集,连字符(-)和点(.)由字符串路径按字面解释。如果你需要在路径字符串中使用美元字符($),请将其包含在([和])中,例如,“/data/$book”处的请求的路径字符串将是“/data/([$])book”。Express使用path-to-regexp来匹配路由路径,有关定义路由路径的所有可能性,请参阅path-to-regexp文档,Express Route Tester是一个用于测试基本Express路由的便捷工具,但它不支持模式匹配。查询字符串不是路由路径的一部分。以下是基于字符串的路由路径的一些示例。此路由路径将匹配对根路由/的请求。app.get(’/’, function (req, res) { res.send(‘root’)})此路由路径将匹配/about的请求。app.get(’/about’, function (req, res) { res.send(‘about’)})此路由路径将匹配对/random.text的请求。app.get(’/random.text’, function (req, res) { res.send(‘random.text’)})以下是基于字符串模式的路由路径的一些示例。此路由路径将匹配acd和abcd。app.get(’/ab?cd’, function (req, res) { res.send(‘ab?cd’)})此路由路径将匹配abcd、abbcd、abbbcd等。app.get(’/ab+cd’, function (req, res) { res.send(‘ab+cd’)})此路由路径将匹配abcd、abxcd、abRANDOMcd、ab123cd等。app.get(’/abcd’, function (req, res) { res.send(‘ab*cd’)})此路由路径将匹配/abe和/abcde。app.get(’/ab(cd)?e’, function (req, res) { res.send(‘ab(cd)?e’)})基于正则表达式的路由路径示例:此路由路径将匹配其中包含“a”的任何内容。app.get(/a/, function (req, res) { res.send(’/a/’)})这个路由路径将与butterfly和dragonfly相匹配,但不会与butterflyman、dragonflyman等相匹配。app.get(/.fly$/, function (req, res) { res.send(’/.fly$/’)})路由参数路由参数是命名的URL片段,用于捕获在URL中的位置指定的值,捕获的值填充在req.params对象中,在路径中指定的路由参数的名称作为其各自的键。Route path: /users/:userId/books/:bookIdRequest URL: http://localhost:3000/users/34/books/8989req.params: { “userId”: “34”, “bookId”: “8989” }要使用路由参数定义路由,只需在路由路径中指定路由参数,如下所示。app.get(’/users/:userId/books/:bookId’, function (req, res) { res.send(req.params)})路由参数的名称必须由“单词字符”([A-Za-z0-9_])组成。由于连字符(-)和点(.)按字面解释,因此它们可以与路由参数一起使用以用于有用的目的。Route path: /flights/:from-:toRequest URL: http://localhost:3000/flights/LAX-SFOreq.params: { “from”: “LAX”, “to”: “SFO” }Route path: /plantae/:genus.:speciesRequest URL: http://localhost:3000/plantae/Prunus.persicareq.params: { “genus”: “Prunus”, “species”: “persica” }要更好地控制路由参数可以匹配的确切字符串,可以在括号(())中附加正则表达式:Route path: /user/:userId(\d+)Request URL: http://localhost:3000/user/42req.params: {“userId”: “42”}因为正则表达式通常是文字字符串的一部分,所以请务必使用额外的反斜杠转义任何\字符,例如\d+。在Express 4.x中,正则表达式中的字符不以通常的方式解释,要解决此问题,请使用{0,}而不是,这可能会在Express 5中修复。路由处理程序你可以提供多个回调函数,其行为类似于中间件来处理请求,唯一的例外是这些回调可能会调用next(‘route’)来绕过剩余的路由回调,你可以使用此机制在路由上施加前置条件,然后在没有理由继续当前路由的情况下将控制权传递给后续路由。路由处理程序可以是函数,函数数组或两者的组合形式,如以下示例所示。单个回调函数可以处理路由,例如:app.get(’/example/a’, function (req, res) { res.send(‘Hello from A!’)})多个回调函数可以处理路由(确保指定next对象),例如:app.get(’/example/b’, function (req, res, next) { console.log(’the response will be sent by the next function …’) next()}, function (req, res) { res.send(‘Hello from B!’)})一组回调函数可以处理路由,例如:var cb0 = function (req, res, next) { console.log(‘CB0’) next()}var cb1 = function (req, res, next) { console.log(‘CB1’) next()}var cb2 = function (req, res) { res.send(‘Hello from C!’)}app.get(’/example/c’, [cb0, cb1, cb2])单独函数和函数数组的组合可以处理路由,例如:var cb0 = function (req, res, next) { console.log(‘CB0’) next()}var cb1 = function (req, res, next) { console.log(‘CB1’) next()}app.get(’/example/d’, [cb0, cb1], function (req, res, next) { console.log(’the response will be sent by the next function …’) next()}, function (req, res) { res.send(‘Hello from D!’)})响应方法下表中的响应对象(res)上的方法可以向客户端发送响应,并终止请求—响应周期,如果没有从路由处理程序调用这些方法,则客户端请求将保持挂起状态。方法描述res.download()提示下载文件res.end()结束响应过程res.json()发送JSON响应res.jsonp()使用JSONP支持发送JSON响应res.render()渲染视图模板res.send()发送各种类型的响应res.sendFile()将文件作为八位字节流发送res.sendStatus()设置响应状态码并将其字符串表示形式作为响应体发送app.route()你可以使用app.route()为路由路径创建可链接的路由处理程序,由于路径是在单个位置指定的,因此创建模块化路由很有帮助,同时减少冗余和拼写错误,有关路由的更多信息,请参阅:Router()文档。以下是使用app.route()定义的链接路由处理程序示例。app.route(’/book’) .get(function (req, res) { res.send(‘Get a random book’) }) .post(function (req, res) { res.send(‘Add a book’) }) .put(function (req, res) { res.send(‘Update the book’) })express.Router使用express.Router类创建模块化、可装载的路由处理程序,Router实例是一个完整的中间件和路由系统,因此,它通常被称为“迷你应用程序”。以下示例将路由器创建为模块,在其中加载中间件功能,定义一些路由,并将路由器模块装载在主应用程序中的路径上。在应用程序目录中创建名为birds.js的路由器文件,其中包含以下内容:var express = require(’express’)var router = express.Router()// middleware that is specific to this routerrouter.use(function timeLog (req, res, next) { console.log(‘Time: ‘, Date.now()) next()})// define the home page routerouter.get(’/’, function (req, res) { res.send(‘Birds home page’)})// define the about routerouter.get(’/about’, function (req, res) { res.send(‘About birds’)})module.exports = router然后,在应用程序中加载路由器模块:var birds = require(’./birds’)// …app.use(’/birds’, birds)该应用程序现在能够处理对/birds和/birds/about的请求,以及调用特定于该路由的timeLog中间件函数。上一篇:常见问题 ...

December 27, 2018 · 2 min · jiezi

Express 文档(常见问题)

常见问题我该如何构建我的应用程序?这个问题没有明确的答案,答案取决于你的应用程序规模和所涉及的团队,为了尽可能灵活,Express在结构方面没有做出任何假设。在你喜欢的任何目录结构中,路由和其他特定于应用程序的逻辑可以存在于你希望的任意数量的文件中,查看以下示例以获取灵感:路由列表路由图MVC风格控制器此外,还有Express的第三方扩展,简化了其中一些模式:资源丰富的路由如何定义模型?Express没有数据库的概念,此概念由第三方Node模块决定,允许你与几乎任何数据库进行交互。请参阅LoopBack,了解以模型为中心的基于Express的框架。如何验证用户身份?身份验证是Express不会冒险的另一个有争议的领域,你可以使用任何你想要的身份验证方案,有关简单的用户名/密码方案,请参阅此示例。Express支持哪些模板引擎?Express支持符合(path、locals,callback)签名的任何模板引擎,要规范化模板引擎接口和缓存,请参阅consolidate.js项目以获取支持,未列出的模板引擎可能仍然支持Express签名。有关更多信息,请参阅使用Express的模板引擎。如何处理404响应?在Express中,404响应不是错误的结果,因此错误处理程序中间件不会捕获它们,这种行为是因为404响应只是表明没有额外的工作要做,换句话说,Express已经执行了所有中间件函数和路由,并发现它们都没有响应,你需要做的就是在堆栈的最底部添加一个中间件函数(在所有其他函数之下)来处理404响应:app.use(function (req, res, next) { res.status(404).send(“Sorry can’t find that!”)})在express.Router()实例上的运行时动态添加路由,这样路由不会被中间件函数取代。如何设置错误处理程序?你可以使用与其他中间件相同的方式定义错误处理中间件,除了使用四个参数而不是三个参数,具体的签名(err, req, res, next):app.use(function (err, req, res, next) { console.error(err.stack) res.status(500).send(‘Something broke!’)})有关更多信息,请参阅错误处理。如何呈现纯HTML?没有必要使用res.render()函数“渲染”HTML,如果你有特定文件,请使用res.sendFile()函数,如果要从目录提供许多资源,请使用express.static()中间件函数。上一篇:静态文件

December 26, 2018 · 1 min · jiezi

Express 文档(静态文件)

静态文件要提供静态文件(如images、CSS文件和JavaScript文件),请使用Express中的express.static内置中间件功能,函数签名是:express.static(root, [options])root参数指定从中提供静态资产的根目录,有关options参数的更多信息,请参阅express.static。例如,使用以下代码在名为public的目录中提供images、CSS文件和JavaScript文件:app.use(express.static(‘public’))现在,你可以加载public目录中的文件:http://localhost:3000/images/kitten.jpghttp://localhost:3000/css/style.csshttp://localhost:3000/js/app.jshttp://localhost:3000/images/bg.pnghttp://localhost:3000/hello.htmlExpress会查找相对于静态目录的文件,因此静态目录的名称不是URL的一部分。要使用多个静态资产目录,请多次调用express.static中间件函数:app.use(express.static(‘public’))app.use(express.static(‘files’))Express按照使用express.static中间件函数设置静态目录的顺序查找文件。注意:为获得最佳结果,请使用反向代理缓存来提高服务静态资产的性能。要为express.static函数提供的文件创建虚拟路径前缀(文件系统中实际不存在路径),请为静态目录指定挂载路径,如下所示:app.use(’/static’, express.static(‘public’))现在,你可以从/static路径前缀加载public目录中的文件。http://localhost:3000/static/images/kitten.jpghttp://localhost:3000/static/css/style.csshttp://localhost:3000/static/js/app.jshttp://localhost:3000/static/images/bg.pnghttp://localhost:3000/static/hello.html但是,你提供给express.static函数的路径是相对于启动node进程的目录,如果从另一个目录运行express应用程序,则使用要提供的目录的绝对路径更安全:app.use(’/static’, express.static(path.join(__dirname, ‘public’)))有关serve-static函数及其选项的更多详细信息,请参阅serve-static。上一篇:路由基础

December 26, 2018 · 1 min · jiezi

Express 文档(路由基础)

路由基础路由是指确定应用程序如何响应对特定端点的客户端请求,该请求是URI(或路径)和特定HTTP请求方法(GET,POST等)。每个路由都可以有一个或多个处理函数,这些函数在路由匹配时执行。路由定义采用以下结构:app.METHOD(PATH, HANDLER)app是express的一个实例。METHOD是一种小写的HTTP请求方法。PATH是服务器上的路径。HANDLER是匹配到路由时执行的函数。本教程假定创建了一个名为app的express实例,并且服务器正在运行,如果你不熟悉创建应用程序并启动它,请参阅Hello world示例。以下示例说明了定义简单路由。在主页上响应Hello World!:app.get(’/’, function (req, res) { res.send(‘Hello World!’)})响应应用程序主页的根路由(/)上的POST请求:app.post(’/’, function (req, res) { res.send(‘Got a POST request’)})响应对/user路由的PUT请求:app.put(’/user’, function (req, res) { res.send(‘Got a PUT request at /user’)})响应对/user路由的DELETE请求:app.delete(’/user’, function (req, res) { res.send(‘Got a DELETE request at /user’)})有关路由的更多详细信息,请参阅路由指南。上一篇:Express生成器下一篇:静态文件

December 26, 2018 · 1 min · jiezi

Express 文档(Express生成器)

Express应用程序生成器使用应用程序生成器工具express-generator快速创建应用程序框架。express-generator包安装了express命令行工具,使用以下命令执行此操作:$ npm install express-generator -g使用-h选项显示命令选项:$ express -h Usage: express [options] [dir] Options: -h, –help output usage information –version output the version number -e, –ejs add ejs engine support –hbs add handlebars engine support –pug add pug engine support -H, –hogan add hogan.js engine support –no-view generate without view engine -v, –view <engine> add view <engine> support (ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade) -c, –css <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css) –git add .gitignore -f, –force force on non-empty directory例如,以下内容创建名为myapp的Express应用程序,该应用程序将在当前工作目录中创建在名为myapp的文件夹中,并且视图引擎将设置为Pug:$ express –view=pug myapp create : myapp create : myapp/package.json create : myapp/app.js create : myapp/public create : myapp/public/javascripts create : myapp/public/images create : myapp/routes create : myapp/routes/index.js create : myapp/routes/users.js create : myapp/public/stylesheets create : myapp/public/stylesheets/style.css create : myapp/views create : myapp/views/index.pug create : myapp/views/layout.pug create : myapp/views/error.pug create : myapp/bin create : myapp/bin/www然后安装依赖项:$ cd myapp$ npm install在MacOS或Linux上,使用以下命令运行应用程序:$ DEBUG=myapp:* npm start在Windows上,使用此命令:> set DEBUG=myapp:* & npm start然后在浏览器中加载http://localhost:3000/以访问该应用程序。生成的应用程序具有以下目录结构:.├── app.js├── bin│ └── www├── package.json├── public│ ├── images│ ├── javascripts│ └── stylesheets│ └── style.css├── routes│ ├── index.js│ └── users.js└── views ├── error.pug ├── index.pug └── layout.pug7 directories, 9 files生成器创建的应用程序结构只是构建Express应用程序的众多方法之一,随意使用此结构或修改它以最好地满足你的需求。上一篇:Hello world下一篇:路由基础 ...

December 26, 2018 · 1 min · jiezi

Express 文档(Hello world)

Hello world 示例下面的代码片段是你可以创建的最简单的Express应用程序,它是一个单一文件的应用程序 — 如果使用Express生成器,就不会得到这样的结果,Express生成器为一个完整的应用程序创建脚手架,其中包含大量JavaScript文件、Jade模板和用于各种目的的子目录。const express = require(’express’)const app = express()const port = 3000app.get(’/’, (req, res) => res.send(‘Hello World!’))app.listen(port, () => console.log(Example app listening on port ${port}!))此应用程序启动服务器并监听端口3000上的连接,对于根URL(/)或路由的请求,应用程序以“Hello World!”响应,对于其他所有路径,它将以404 Not Found响应。首先创建一个名为myapp的目录,进入并运行npm init,然后根据安装指南安装express作为依赖项。在myapp目录中,创建一个名为app.js的文件,并复制上面示例中的代码。req(请求)和res(响应)是Node提供的完全相同的对象,因此你可以调用req.pipe()、req.on(‘data’, callback)以及在没有Express参与的情况下执行的任何其他操作。使用以下命令运行应用程序:$ node app.js然后,在浏览器中加载http://localhost:3000/以查看输出。上一篇:安装下一篇:Express生成器

December 26, 2018 · 1 min · jiezi

Express 文档(安装)

安装假设你已经安装了Node.js,请创建一个目录来保存你的应用程序,并将其作为你的工作目录。$ mkdir myapp$ cd myapp使用npm init命令为你的应用程序创建package.json文件,有关package.json如何工作的更多信息,请参阅npm的package.json处理的细节。$ npm init此命令会提示你输入许多内容,例如应用程序的名称和版本,现在,你可以简单地按RETURN接受其中大多数的默认值,但以下情况除外:entry point: (index.js)输入app.js,或者你想要的主文件名称,如果你希望它是index.js,请按RETURN接受建议的默认文件名。现在在myapp目录中安装Express并将其保存在依赖项列表中,例如:$ npm install express –save要临时安装Express而不将其添加到依赖项列表:$ npm install express –no-save默认情况下,版本为npm 5.0+ npm install将模块添加到package.json文件中的依赖项列表中,对于早期版本的npm,你必须明确指定–save选项,然后,在app目录中运行npm install将自动在依赖项列表中安装模块。上一篇:Express 文档(目录)

December 26, 2018 · 1 min · jiezi

Express 文档(目录)

Express 文档基于Node.js的快速、开发、极简主义的Web框架,Express是一个最小且灵活的Node.js Web应用程序框架,为Web和移动应用程序提供了一组强大的功能。借助无数的HTTP实用程序方法和中间件,你可以快速轻松地创建强大的API。Express提供了一层轻薄的基本Web应用程序功能,而不会隐藏你熟悉和喜爱的Node.js功能。入门安装Hello worldExpress生成器基础路由静态文件常问问题指南路由编写中间件使用中间件使用模板引擎错误处理调试代理后面的Express搬到Express 4搬到Express 5数据库集成API参考express()ApplicationRequestResponseRouter高级主题模板引擎进程管理安全更新安全性最佳实践性能最佳实践健康检查和优雅的关闭

December 26, 2018 · 1 min · jiezi

Express文件表单解析中间件 Multer简介

前言Express中最常使用的form解析中间件就是body-parser了,但是它明确表示不会支持multipart/form-data类型的表单.所以在body-parser官方文档中提供了如下的几个支持multipart/form-data类型的中间件的链接,或者只支持multipart/form-data解析的中间件链接.名称&地址周下载量starsbusboy426,2781448multipart240,921993formidable1,390,3614735multer284,9265860统计截止到2018年12月26日multer依赖busboy所以所以busboy的实际直接下载数量应该要减少28万????.什么是multipart/form-data类型的表单?最直观的解释就是支持上传文件的form表单,如果不使用JavaScript中创建的话,显式的html声明如下:<form action="/profile" method=“post” enctype=“multipart/form-data”> <input type=“file” name=“file”/> <input type=“submit” value=“submit”></form>上例子中的<input type=“file” name=“file” >在页面中的显示就是为一个按钮点击后可以进行文件选择.其次input可以添加multiple=“multiple"属性,这个时候打开的文件选择框会允许多选文件.总结一下有如下几种关系:一个name对应多个文件一个name对应一个文件多个name对应多个单个文件多个name对应多个文件顺便说一句multer有中文文档.正文特点multer只会解析form设置为enctype=“multipart/form-data"表单.multer可以定制存储引擎multer会将上传的信息以及内容挂载到request对象上request.body 保存文本内容request.file 保存单个文件信息以及对应内容(内存存储模式)request.files 保存多个文件信息以及对应的内容(内存存储模式)基本工作流程创建一个multer实例使用该实例上提供的不同方法获取不同功能的中间件放入到对应的路由中上传单个文件实例引入multer和Expressconst express = require(’express’), multer = require(‘multer’), app = express();传入配置参数const upload = multer({dest:’/uploads’});注意:dest参数指定了文件输出的位置,可以详细指定文件输出以及储存后面会将.使用multer中间件传递单个文件app.get(’/’,(request,response)=>{ console.log(‘get.request.body’,request.body); console.log(‘get.request.file’,request.file); console.log(‘get.request.files’,request.files); response.send(’<form action=”/” enctype=“multipart/form-data” method=“post”>’+ ‘<input type=“text” name=“title”><br>’+ ‘<input type=“file” name=“upload” multiple=“multiple”><br>’+ ‘<input type=“submit” value=“Upload”>’+ ‘</form>’)});app.post(’/’,upload.single(‘upload’),(request,response)=>{ console.log(‘post.request.body’,request.body); console.log(‘post.request.file’,request.file); console.log(‘post.request.files’,request.files); response.redirect(’/’);});app.listen(8888,()=>{ console.log(’express正在监听8888端口’);});这个例子中我们监听了根路径,分别处理两种不同的请求方式,针对get我们响应表单,针对post我们接受上传的内容.注意:upload.single(‘upload’)意思是高速multer只接收name是upload的单个文件.注意:这个例子中input是可以进行多选的,也就是说后端指定了文件数量为1但是页面依然上传了多个,这个时候multer会报错.注意:dest指定的路径为upload/会将文件保存到根路径下的upload文件夹中,对于windows系统来说是在运行这个应用对应的盘符下例如F:\uploads\这个例子中我填写了一个文本内容,同时上传了一个文件,输出结果如下:post.request.body { title: ‘hello world’ }post.request.file { fieldname: ‘upload’, originalname: ‘硬盘坏道扫描及修复工具Victoria.7z’, encoding: ‘7bit’, mimetype: ‘application/octet-stream’, destination: ‘/uploads’, filename: ‘6bdfc0df998d72e6232d60f790f47ef8’, path: ‘\uploads\6bdfc0df998d72e6232d60f790f47ef8’, size: 1033375 }上传多个文件实例const express = require(’express’), multer = require(‘multer’), app = express();const upload = multer({dest:’/uploads’});app.get(’/’,(request,response)=>{ console.log(‘get.request.body’,request.body); console.log(‘get.request.file’,request.file); console.log(‘get.request.files’,request.files); response.send(’<form action="/" enctype=“multipart/form-data” method=“post”>’+ ‘<input type=“text” name=“title”><br>’+ ‘<input type=“file” name=“upload”><br>’+ // 此处有两个相同name的input ‘<input type=“file” name=“upload”><br>’+ ‘<input type=“submit” value=“Upload”>’+ ‘</form>’)});app.post(’/’,upload.array(‘upload’),(request,response)=>{ // 注意此处使用的中间件和上例中不同 console.log(‘post.request.body’,request.body); console.log(‘post.request.file’,request.file); console.log(‘post.request.files’,request.files); response.redirect(’/’);});app.listen(8888,()=>{ console.log(’express正在监听8888端口’);});在这个例子的表单中有两个同名的name都是文件类型,这次使用array的方式来进行接受,控制台输出内容如下:post.request.body { title: ‘hello world’ }post.request.file undefinedpost.request.files [ { fieldname: ‘upload’, originalname: ‘硬盘坏道扫描及修复工具Victoria.7z’, encoding: ‘7bit’, mimetype: ‘application/octet-stream’, destination: ‘/uploads’, filename: ‘71ed2ac4299d43a30f5c13892f33e51b’, path: ‘\uploads\71ed2ac4299d43a30f5c13892f33e51b’, size: 1033375 }, { fieldname: ‘upload’, originalname: ‘新建文本文档.txt’, encoding: ‘7bit’, mimetype: ’text/plain’, destination: ‘/uploads’, filename: ‘190bde8fcdd08d57648ffb243607ed9d’, path: ‘\uploads\190bde8fcdd08d57648ffb243607ed9d’, size: 218 } ]在上面的例子中删除掉一个input,将剩余的input添加multiple属性用于多选,页面中在选择文件框中选择多个文件也是可以顺利通过.其余的方法除了上方提到的multer.single方法外还有其他的几种方法.array(name:string,maxcount?:number) 根据name限制上传文件的最大个数fields(fields:object) 自定义限制规则none() 只保留文本信息any() 允许任意类型通过,文件数组将保存在 req.files实际上它们都可以视为fields方法的包装,源码如下:Multer.prototype.single = function (name) { return this._makeMiddleware([{ name: name, maxCount: 1 }], ‘VALUE’)}Multer.prototype.array = function (name, maxCount) { return this._makeMiddleware([{ name: name, maxCount: maxCount }], ‘ARRAY’)}Multer.prototype.fields = function (fields) { return this._makeMiddleware(fields, ‘OBJECT’)}Multer.prototype.none = function () { return this._makeMiddleware([], ‘NONE’)}Multer.prototype.any = function () { function setup () { return { limits: this.limits, preservePath: this.preservePath, storage: this.storage, fileFilter: this.fileFilter, fileStrategy: ‘ARRAY’ } } return makeMiddleware(setup.bind(this))}常用API一览来自中文文档:multer(opts)Multer 接受一个 options 对象,其中最基本的是 dest 属性,这将告诉 Multer 将上传文件保存在哪。如果你省略 options 对象,这些文件将保存在内存中,永远不会写入磁盘。为了避免命名冲突,Multer 会修改上传的文件名。这个重命名功能可以根据您的需要定制。以下是可以传递给 Multer 的选项。KeyDescriptiondest or storage在哪里存储文件fileFilter文件过滤器,控制哪些文件可以被接受limits限制上传的数据preservePath保存包含文件名的完整文件路径fileFilter设置一个函数来控制什么文件可以上传以及什么文件应该跳过,这个函数应该看起来像这样:function fileFilter (req, file, cb) { // 这个函数应该调用 cb 用boolean值来 // 指示是否应接受该文件 // 拒绝这个文件,使用false,像这样: cb(null, false) // 接受这个文件,使用true,像这样: cb(null, true) // 如果有问题,你可以总是这样发送一个错误: cb(new Error(‘I don't have a clue!’))}错误处理机制当遇到一个错误,multer 将会把错误发送给 express。你可以使用一个比较好的错误展示页 (express标准方式)。如果你想捕捉 multer 发出的错误,你可以自己调用中间件程序。如果你想捕捉 Multer 错误,你可以使用 multer 对象下的 MulterError 类 (即 err instanceof multer.MulterError)。var multer = require(‘multer’)var upload = multer().single(‘avatar’)app.post(’/profile’, function (req, res) { upload(req, res, function (err) { if (err instanceof multer.MulterError) { // 发生错误 } else if (err) { // 发生错误 } // 一切都好 })})磁盘存储引擎 (DiskStorage)磁盘存储引擎可以让你控制文件的存储。var storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, ‘/tmp/my-uploads’) }, filename: function (req, file, cb) { cb(null, file.fieldname + ‘-’ + Date.now()) }})var upload = multer({ storage: storage })有两个选项可用,destination 和 filename。他们都是用来确定文件存储位置的函数。destination 是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个 string (例如 ‘/tmp/uploads’)。如果没有设置 destination,则使用操作系统默认的临时文件夹。注意: 如果你提供的 destination 是一个函数,你需要负责创建文件夹。当提供一个字符串,multer 将确保这个文件夹是你创建的。filename 用于确定文件夹中的文件名的确定。 如果没有设置 filename,每个文件将设置为一个随机文件名,并且是没有扩展名的。注意: Multer 不会为你添加任何扩展名,你的程序应该返回一个完整的文件名。每个函数都传递了请求对象 (req) 和一些关于这个文件的信息 (file),有助于你的决定。注意 req.body 可能还没有完全填充,这取决于向客户端发送字段和文件到服务器的顺序。内存存储引擎 (MemoryStorage)内存存储引擎将文件存储在内存中的 Buffer 对象,它没有任何选项。var storage = multer.memoryStorage()var upload = multer({ storage: storage })当使用内存存储引擎,文件信息将包含一个 buffer 字段,里面包含了整个文件数据。警告: 当你使用内存存储,上传非常大的文件,或者非常多的小文件,会导致你的应用程序内存溢出。filefilter和filename还有路由触发的顺序const express = require(’express’), multer = require(‘multer’), app = express();const storage = multer.diskStorage({ destination:__dirname, // 保存到当前目录 filename(request,file,callback){ console.log(‘filename:’,file); callback(null,’newfilename’);// 修改上传的文件名称 }});const upload = multer({ dest:’/uploads’, fileFilter(request,file,cb){ console.log(‘fileFilter:’,file); cb(null,true); }, limits:{ fileSize:100000 // 限制上传文件大小为100000字节 }, storage // 使用默认的储存器});最后触发的顺序为:filefilterfilename我们定义的路由 ...

December 26, 2018 · 2 min · jiezi

理解vue ssr原理,自己搭建简单的ssr框架

前言大多数Vue项目要支持SSR应该是为了SEO考虑,毕竟对于WEB应用来说,搜索引擎是一个很大的流量入口。Vue SSR现在已经比较成熟了,但是如果是把一个SPA应用改造成SSR应用,成本还是有些高的,这工作量无异于重构前端。另外对前端的技术要求也是挺高的,需要对Vue比较熟悉,还要有Node.js 和 webpack 的应用经验。引入Vue是一个构建客户端应用的框架,即vue组件是在浏览器中进行渲染的。所谓服务端渲染,指的是把vue组件在服务器端渲染为组装好的HTML字符串,然后将它们直接发送到浏览器,最后需要将这些静态标记"激活"为客户端上完全可交互的应用程序。服务端渲染的优点更好的SEO,搜索引擎爬虫可以抓取渲染好的页面更快的内容到达时间(首屏加载更快),因为服务端只需要返回渲染好的HTML,这部分代码量很小的,所以用户体验更好服务端渲染的缺点首先就是开发成本比较高,比如某些声明周期钩子函数(如beforeCreate、created)能同时运行在服务端和客户端,因此第三方库要做特殊处理,才能在服务器渲染应用程序中运行。由于服务端渲染要用Nodejs做中间层,所以部署项目时,需要处于Node.js server运行环境。在高流量环境下,还要做好服务器负载和缓存策略原理解析先附上demo地址:https://github.com/wmui/vue-s…第一步:编写entry-client.js和entry-server.jsentry-client.js只在浏览器环境下执行,所以需要显示调用$mount方法,挂载DOM节点import Vue from ‘vue’;import App from ‘./App.vue’;import createStore from ‘./store/index.js’;function createApp() { const store = createStore(); const app = new Vue({ store, render: h => h(App) }); return {app, store}}const { app, store } = createApp();// 使用window.__INITIAL_STATE__中的数据替换整个state中的数据,这样服务端渲染结束后,客户端也可以自由操作state中的数据if (window.INITIAL_STATE) { store.replaceState(window.INITIAL_STATE);}app.$mount(’#app’);entry-server.js需要导出一个函数,在服务端渲染期间会被调用import Vue from ‘vue’;import App from ‘./App.vue’;import createStore from ‘./store/index.js’;export default function(context) { // context是上下文对象 const store = createStore(); let app = new Vue({ store, render: h => h(App) }); // 找到所有 asyncData 方法 let components = App.components; let asyncDataArr = []; // promise集合 for (let key in components) { if (!components.hasOwnProperty(key)) continue; let component = components[key]; if (component.asyncData) { asyncDataArr.push(component.asyncData({store})) // 把store传给asyncData } } // 所有请求并行执行 return Promise.all(asyncDataArr).then(() => { // context.state 赋值成什么,window.INITIAL_STATE 就是什么 // 这下你应该明白entry-client.js中window.__INITIAL_STATE__是哪来的了,它是在服务端渲染期间被添加进上下文的 context.state = store.state; return app; });};上面的asyncData是干嘛用的?其实,这个函数是专门请求数据用的,你可能会问请求数据为什么不在beforeCreate或者created中完成,还要专门定义一个函数?虽然beforeCreate和created在服务端也会被执行(其他周期函数只会在客户端执行),但是我们都知道请求是异步的,这就导致请求发出后,数据还没返回,渲染就已经结束了,所以无法把 Ajax 返回的数据也一并渲染出来。因此需要想个办法,等到所有数据都返回后再渲染组件asyncData需要返回一个promise,这样就可以等到所有请求都完成后再渲染组件。下面是在foo组价中使用asyncData的示例,在这里完成数据的请求export default { asyncData: function({store}) { return store.dispatch(‘GET_ARTICLE’) // 返回promise }, computed: { article() { return this.$store.state.article } }}第二步:配置webpackwebpack配置比较简单,但是也需要针对client和server端单独配置webpack.client.conf.js显然是用来打包客户端应用的module.exports = merge(base, { entry: { client: path.join(__dirname, ‘../entry-client.js’) }});webpack.server.conf.js用来打包服务端应用,这里需要指定node环境module.exports = merge(base, { target: ’node’, // 指定是node环境 entry: { server: path.join(__dirname, ‘../entry-server.js’) }, output: { filename: ‘[name].js’, // server.js libraryTarget: ‘commonjs2’ // 必须按照 commonjs规范打包才能被服务器调用。 }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname, ‘../index.ssr.html’), filename: ‘index.ssr.html’, files: { js: ‘client.js’ }, // client.js需要在html中引入 excludeChunks: [‘server’] // server.js只在服务端执行,所以不能打包到html中 }) ]});第三步:启动服务打包完成后就可以启动服务了,在start.js中我们需要把server.js加载进来,然后通过renderToString方法把渲染好的html返回给浏览器const bundle = fs.readFileSync(path.resolve(__dirname, ‘dist/server.js’), ‘utf-8’);const renderer = require(‘vue-server-renderer’).createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, ‘dist/index.ssr.html’), ‘utf-8’) // 服务端渲染数据});server.get(’*’, (req, res) => { renderer.renderToString((err, html) => { // console.log(html) if (err) { console.error(err); res.status(500).end(‘服务器内部错误’); return; } res.end(html); })});效果图demo已经上传到github: http://github.com/wmui/vue-ss…结语个人实践Vue SSR已有一段时间,发现要想搭建一套完整的 SSR 服务框架还是很有挑战的,或许 Nuxt 是一个不错的选择,对 Nuxt 感兴趣的朋友可以参考我的一个开源小作品Essay以上,感谢阅读! ...

December 22, 2018 · 2 min · jiezi

react+express项目

1 开发环境准备(windows)1.1 安装nodejs和npm1) 下载nodejs安装包:http://nodejs.org/en/download/nodejs安装时会同时安装npm2) 安装完成后检查是否安装成功命令行输入以下命令,查看npm和node版本:npm -vnode -v 若未安装成功可检查环境变量是否安装时自动设置成功1.2 安装create-react-app(react官方提供的脚手架) 命令行输入:npm install -g create-react-appcreate-react-app:可以用来快速创建react项目-g:全局安装create-react-app脚手架工具,这个步骤只需要执行一次1.3 安装express-generator(express官方提供脚手架) 命令行输入: npm install express-generator -gexpress-generator:可以用来快速创建express应用-g:全局安装express-generator脚手架工具,这个步骤只需要执行一次2 创建react+express项目前端框架:react服务端:基于node的express框架 两者结合快速创建web项目。由于服务端代码需要部署到服务器,为了方便操作,先创建react项目,然后在react项目目录下创建express项目,将react的打包目录设置为express项目下的public文件。(1)创建react项目(client)create-react-app myapp(2)创建express项目(server)cd myappexpress-generator –view=ejs server 添加模版引擎:–view=ejs,此处选择ejs作为模版引擎。还可以选择pub、jade等其它模版引擎2.2 react初始项目目录结构 使用create-react-app创建的项目,已经把webpack、babel等配置都封装到依赖项目react-script中,因此在目录外层无法看到webpack等配置文件。1)自动生成的项目目录介绍A. node_modules:项目依赖包目录,使用npm install xxx相关命令安装的依赖都会自动下载到该目录,无需提交至git;B. public:公共目录,该目录下的文件都不会被webpack进行加载、解析和打包;通过npm run build进行打包时该目录下的所有文件将会直接被复制到build目录下;a) favicon.ico:网站图标(可替换删除)b) index.html:页面模板,webpack打包后将输出文件引入到该模板内;index.html中通过环境变量%PUBLIC_URL%指向public目录路径;c) manifest.json:PWA将应用添加至桌面的功能的实现依赖于manifest.json。通过manifest.json文件可以对C. src: 是源码目录,该目录下除了index.js App.test.js registerServiceWorker.js 文件具有一定意义其余文件都是演示使用可直接删除。a) index.js: 是整个项目的入口文件,也是webpack打包的入口文件;b) App.js:项目创建后,可通过修改此文件来修改页面内容c) App.test.js: 测试单元演示文件,暂时并不知道干嘛用;可以直接删除;d) registerServiceWorker.js: service worker 是在后台运行的一个线程,可以用来处理离线缓存、消息推送、后台自动更新等任务;registerServiceWorker就是为react项目注册了一个service worker,用来做资源的缓存,这样你下次访问时,就可以更快的获取资源。而且因为资源被缓存,所以即使在离线的情况下也可以访问应用(此时使用的资源是之前缓存的资源)。注意,registerServiceWorker注册的service worker 只在生产环境中生效,并且该功能只有在https下才能有效果;D. .gitignore: 该文件是github过滤文件配置,即指定无需提交至git而忽略的文件,帮助查看方式,git help ignoreE. README.md: 该文件是描述文件F. package.json: 定义了项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。部分依赖模块被隐藏;G. package.lock.json: 每次通过npm添加依赖或者更新包版本时 npm都会把相关版本信息写入package.lock.json文件;2)package.json配置3)可用脚本命令说明首先说明:通过npm run 执行下面命令实际上是运行 node_modules/react-srcipt/script下对应的脚本文件;A.npm run start : 开始项目,通过http://localhost:3000 可访问项目;B. npm run build : 项目打包,在生产环境中编译代码,并放在build目录中;所有代码将被正确打包,并进行优化、压缩同时使用hash重命名文件;执行该命令前需要在package.json中新增条配置"homepage": “."(上面配置文件已给出), 同时build后的项目需要在服务器下才能访问;否则打开的将是空白页面;C. npm run test : 交互监视模式下启动测试运行程序;D. npm run eject : 将隐藏的配置导出;需要知道的是create-react-app脚手架本质上是使用react-scripts进行配置项目,所有配置文件信息都被隐藏起来(node_modules/react-scripts);当需要手动修改扩展webpack配置时有时就需要将隐藏的配置暴露出来;特别需要注意的是该操作是一个单向操作,一旦使用eject,那么就不能恢复了(再次将配置隐藏);2.3 react项目配置 create-react-app默认生成的是单入口单出口生产环境,统一通过react-script进行管理,无法满足复杂的多入口项目的需要,因此需要对项目进行配置,使其满足实际项目需要。可通过npm run eject来暴露所有内建配置,以方便我们对项目的配置进行修改。2.3.1 npm run eject 进入myapp根目录,执行以下命令:npm run eject。暴露所有内建配置,项目下会新增或对部分配置文件进行修改。 根目录下新增config(配置文件)和script(脚本文件)目录。注意:此操作不可逆,一旦执行无法回退;修改配置的其它方法:也可考虑采用react-app-rewired插件来实现配置覆盖。2.3.2 多入口配置项目默认只有index.js(src目录下)这一个入口文件。以在src目录下新增入口文件admin.js为例。需修改config中的配置文件来:1)修改webpcak.config.dev.js文件A. 修改entry(新增js文件入口配置)//这里我已经写成对象格式了//有多少个页面就添加多少个key:value//这里我已经添加了一个admin//数组中的paths.appSrc+’/admin.js’就是这个html页面的入口文件 entry: { index:[ require.resolve(’./polyfills’), require.resolve(‘react-dev-utils/webpackHotDevClient’), paths.appIndexJs, ], admin:[ require.resolve(’./polyfills’), require.resolve(‘react-dev-utils/webpackHotDevClient’), paths.appSrc + ‘/admin.js’, ] }B. 修改plugins中的HtmlWebpackPlugin(配置html模版文件)//多少个页面就new 多少个 HtmlWebpackPlugin //并且在每一个里面的chunks都需要和上面的entry中的key匹配//例如上面entry中有index和admin这两个。//这里的chunks也需要是index和admin new HtmlWebpackPlugin({ inject: true, chunks:[“index”], template: paths.appHtml, }), new HtmlWebpackPlugin({ inject: true, chunks:[“admin”], template:paths.appHtml, filename:‘admin.html’ }),C.修改output//由于原配置入口文件只有一个,因此output中的filename是写死的,//增加多入口之后,输出文件名被写死,对应生成了多个boundle.js,//后面生成的会覆盖前面生成的文件,所以需要制定输出的文件名不能写死output: { path:paths.appBuild, pathinfo: true, filename: ‘static/js/[name].bundle.js’, chunkFilename: ‘static/js/[name].chunk.js’, publicPath: publicPath, devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\/g, ‘/’), },2) 修改config下webpack.config.prod.js文件A 修改entry//这里的paths.appIndexJs和paths.appSrc+’/admin.js’是入口文件 entry:{ index:[ require.resolve(’./polyfills’), paths.appIndexJs ], admin:[ require.resolve(’./polyfills’), paths.appSrc+’/admin.js’ ] }B 修改plugins中的HtmlWebpackPlugin//和开发环境下一样,多少个html就new多少个 HtmllWebpackPlugin,每个都需要指定chunks,并且指定filename,在minify中配置是否压缩js、css等,这是生产环境下的配置 new HtmlWebpackPlugin({ inject: true, chunks:[“index”], template: paths.appHtml, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, }), new HtmlWebpackPlugin({ inject: true, chunks:[“admin”], template: paths.appHtml, filename:‘admin.html’, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, }),3)在开发环境中如果想通过地址访问不同页面,需要修改webpackDevServer.config.jsA 修改historyApiFallback//这里的rewrites:[ {from: /^/admin.html/, to: ‘/build/admin.html’ }] 数组里面是一个个对象,//对象中前面的值是在开发时候访问的路径,例如 npm run start之后会监听 localhost:3000 ,//此时在后面加上 /admin.html就会访问admin.html中的内容,默认是访问index.html;//数组中的第二个值是生产环境下的文件的路径。//如果有很多页面,就在rewrites中添加更多对象 historyApiFallback: { disableDotRule: true, rewrites: [ { from: /^/admin.html/, to: ‘/build/admin.html’ }, ] },2.3.3 前端跨域问题配置生产环境:本文中的项目,由于打包后的代码会放在server目录下的public文件夹下,也就是打包后的代码和server在同域下,不存在跨域问题。开发环境:开发时,前端react项目和后端express项目运行时端口端口不同,存在跨域问题。开发环境跨域问题解决办法:在package.json中加入:“proxy”:http://localhost:5000 //后端所在域。如果需要后端存在多个域://package.json中加入 “proxy”: { “/api1”: { “target”: “http://api1.xxxx.com”, “changeOrigin”:true }, “/api2”:{ “target”:“http://api2.xxxx.com”, “changeOrigin”:true } }2.3.4 文件路径简化配置 当页面嵌套过深时,import Apis from ‘../../common/apis’,可通过webpack配置来简化路径。//修改webpack.config.dev与webpack.config.prod两个文件,加入相同配置//增加方法function resolve(dir) { return path.join(__dirname, ‘..’, dir) }//修改 alias配置alias: { ‘react-native’: ‘react-native-web’, //加入配置 ‘@src’: resolve(‘src’)}添加上述配置后,引入文件方式:import Apis from ‘@src/common/apis’缺点:此方法能简化引用方法,但无法通过快捷键进入该引用文件。2.3.5 webpack打包体积详情分布1)安装:npm install –save-dev webpack-bundle-analyzer2)配置://修改webpack.prod.conf.js文件,增加如下内容const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin;module.exports = { plugins: [ new BundleAnalyzerPlugin() ]}//修改package.json文件,在scripts中增加如下命令“analyz”: “NODE_ENV=production npm_config_report=true npm run build”3)运行npm run build或npm run analyz,浏览器会自动打开127.0.0.1:8888,如下页面,可查看打包后文件分布,以及打包文件大小。2.3.6 webpack构建打包优化1)使用UglifyJSPlugin压缩js文件安装方法: npm install uglifyjs-webpack-plugin –save-dev 在webpack.config.prod.js文件中添加const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin’)module.exports = { plugins: [ new UglifyJSPlugin(), ]}2)生产环境去掉map文件:缩短build时间//修改webpack.config.prod.js文件://注释devtool:shouldUseSourceMap? ‘source-map’:falsedevtool:false,//增加3)添加cache-loader,减少打包时间//修改webpack.config.dev.js文件:module:{ rules:[{ use:[ //添加在最前面 ‘cache-loader’, ] }]}在其它加载程序加载之前添加以将结果缓存在磁盘上4)提取公共包:提取多个入口引入的公共依赖包修改webpack.config.prod.js文件//修改entry文件,entry: //这里的paths.appIndexJs和paths.appSrc+’/admin.js’依然是每个html的入口文件{ index:[ require.resolve(’./polyfills’), paths.appIndexJs, ], admin:[ require.resolve(’./polyfills’), paths.appSrc+’/admin.js’ ], //增加vendor vendor:[‘react’,‘react-dom’]},//修改pluginplugin:{ //新增以下代码 new webpack.optimize.CommonsChunkPlugin({ name: [“vendor”], // filename:‘static/js/vendor.[chunkhash:8].js’, // minChunks: 3 //三方库在逻辑代码中被调用两次(数字可以自定义), 将公共的代码提取出来 }),/* 防止 vendor hash 变化 */ // extract webpack runtime and module manifest to its own file in order to // prevent vendor hash from being updated whenever app bundle is updated new webpack.optimize.CommonsChunkPlugin({ name: ‘manifest’, chunks: [‘vendor’] }),}//修改plugins中的HtmlWebpackPlugin,在chunks中添加需要引入的公共包,//其中公共包需放在后面,使其在加入html页面时能在其它js文件前面 new HtmlWebpackPlugin({ inject: true, chunks:[“index”,“vendor”], template: paths.appHtml, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, },}),new HtmlWebpackPlugin({ inject: true, chunks:[“admin”,“vendor”], template: paths.appHtml, filename:‘admin.html’, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, },}),2.3.7 build命令 命令行输入:npm run build,出现以下文件夹,其中admin.html和index.html分别是不同的入口。2.3.8 修改build后资源文件根路径 可在path.js文件中修改打包后资源文件路径,例如修改path.js文件中getServedPath方法中的‘/’,改为‘/build’,则打包后资源文件的路径会加上build,修改前资源文件路径中是没有build的。 本文中react+express的项目,无需修改资源文件根路径,因为express会配置资源文件所在目录为。2.4 express项目配置(与react结合)2.4.1 nodemon热启动 express框架中启动项目后,文件更新后需要手动重启node服务,修改才会生效。使用nodemon可以实现热启动,文件修改后自动重启使修改生效。 在server根目录(express项目根目录)下运行以下命令,安装nodemon:npm –save-dev install nodemon//修改server(express项目根目录)目录下的package.json文件,将node改为nodemon//将"start”:“node ./bin/www"改为:“start”:“nodemon ./bin/www"2.4.2 react打包目录相关修改 为了方便,将react打包目录修改为server目录下public目录,可以避免每次打包后都需要将build目录下的文件在复制到server目录下。//修改path.js文件,module.exports中的appBuild变量//将appBuild: resolveApp(‘build’),改为appBuild: resolveApp(‘server/public’),2.4.3 在react的package.json中增加server的启动命令 在webstorm中,会自动出现根目录下package.json中的scripts下的npm命令,为了方便启动server,可在react 根目录下的package.json文件中增加server的启动项。“scripts”: { “start”: “node scripts/start.js”, “server-start”: “cd server && npm run start”,//增加server启动命令 “build”: “node scripts/build.js”, “test”: “node scripts/test.js” },2.5 react开发调试工具2.5.1 react-developer-tools 浏览器扩展工具中搜索此插件并安装,可以查看到react组件结构2.5.2 chrome下的source map chrome引入了source-map文件,可以查看打包前代码。唯一要做的就是配置webpack自动生成source-map文件,这也很简单,在webpack.config.js中增加一行配置即可(需要重新启动webpack-dev-server使配置生效),create-react-app已做此配置,因此不需要再修改。2.5.3 Eslint(javascript代码检查工具) Create-react-app已安装Eslint,可对eslint进行自定义配置规则。2.6 项目目录结构优化2.6.1 react项目目录结构优化 开发目录主要是src目录,因此需要修改的目录主要是src目录。|——src|————|common //公共组件目录,如http.js、cookie.js等|————|components //基础组件、业务组件、业务代码抽象出的一些基础类,例如每个页面可以在此目录下建立一个文件存放相关组件。|————|layouts //布局相关组件及其样式文件,如header.js、footer.js、menu.js等|————|styles //公共样式文件|————|static //公共的静态资源文件,如公共的图片资源文件等|————|views //页面入口文件,可与comonents中的页面组件对应如果使用了router和redux可在src下增加目录:redux:redux应用数据状态管理文件,包括actions、reducers、stores三个子目录routes:路由管理模块containers:应用根容器,用于连接redux和router2.6.2 express项目目录结构优化|——server // express项目根目录|————|bin |——————|www //服务器相关配置文件|————|controllers //控制器层,处理前端请求|————|models //数据库操作相关文件|————|node_modules //npm包安装目录|————|public //react打包目录,存放所有的html,js/css/图片等资源文件|————|routes // 路由文件目录|——————|api.js //api请求路由文件|——————|pages.js // 页面请求路由文件|————|utils // 公共文件目录|——————|config.js //各种常量或公共方法|——————|db.js // 数据库访问方法封装|——————|http.js //http请求方法封装|————|views // express框架自带,由于打包后的文件全放在public目录下,因此这个文件可不用了|————|app.js //入口文件|————|package.json |————|package-lock.json ...

December 21, 2018 · 3 min · jiezi

示例Express中路由规则及获取请求参数

本次给大家分享一篇基于express中路由规则及获取请求参数的方法,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。express中常见的路由规则主要使用的路由规则是get和post两种,即var express = require(’express’);var app = express();app.get(); // get和post两种请求方式app.post();//欢迎加入前端全栈开发交流圈一起学习交流:864305860app.get()和app.post()的第一个参数为请求路径,第二个参数为处理请求的回调函数;回调函数有两个参数,分别为req和res,代表请求信息和响应信息。获取请求路径和请求体中的各种参数路径请求及对应获取请求路径的形式有以下几种:(1)req.query (查询get请求中的参数)GET /shoes?order=desc&shoe[type]=converse&shoe[color]=bluereq.query.order// =>‘desc’req,query.shoe.type// =>‘converse’(2)req.body (查询请求体)// POST user[name]=dby&user[email]=bing@163.comreq.body.user.name// =>‘dby’(3)req.params// GET /file/javascript/jquery.jsreq.params[0]// => ‘javascript/jquery.js’(4)req.params(name)// ?name=tobireq.params(name)// => ’tobi’// POST name=tobireq.param(’name’)// => ’tobi’//欢迎加入前端全栈开发交流圈一起学习交流:864305860由上述代码可以很明显的看出各种获取路径的含义:req.query: 处理get请求,获取get请求的请求参数req.params: 处理/:xxx形式的get或者post请求,获取请求参数req.body: 处理post请求,获取post了请求的请求体req.param(): 处理get和post请求,但查找优先级由高到低为req.params->req.body->req.query注:路径规则支持正则表达式。结语感谢您的观看,如有不足之处,欢迎批评指正。

December 17, 2018 · 1 min · jiezi

大家好 这就是2018年的我~

大家好,今天周五,明天就是周末,再过几天也就是2019,2018即将成为过去,昨晚抽时间对自己的2018做了个年终总结,今天跟大家汇报一下。以下就是2018年的我:首先请看看我的钱包接下来就是我的积蓄然后就是我的体重还有大家比较关心的我的睡眠质量啦啦~ 我的发量,默默问一句,你的有多少?我对生活的态度佛系青年有没有,是不是很棒棒??我的感情经历相信自己的眼睛,你没有看错、网络也正常??我的生活经历字不重要,图是重点??我的求知欲学习使我进步,我对知识的渴望……我的夜生活身处一线城市,丰富多彩的夜生活……抱歉 上面那张图是我比较期望的这才是我的夜生活~我的理想残酷的现实我的养生理念人人都爱美,我也有自己的养生理念我的自信心我的上班状态热爱工作的我,干劲十足~我的下班状态好烦,怎么这么快就下班了~开会时的我听到八卦的我我的朋友圈年初的我新的一年开始了,一定要大展身手年终的我总的来说我的2018年大概是这样的这样的这样的这样的和这样的真是丰富而又充实的一年呢!好啦,2018终于过完啦,大概就是这样,2019,请千万对我好一点啊~~2019,请千万对我好一点啊~~今年的冬天特别的冷,希望此文能博你一笑,如果你喜欢的话,欢迎点赞、关注,分享给你的好友们就更棒了,关注公号(IT平头哥联盟)谢谢支持~其他vuereactjava等资源共享 团队解散,我们该何去何从?月入三万 还能少了你一个鸡蛋如何给localStorage设置一个有效期?作者:苏南 - 首席填坑官链接:http://susouth.com/交流:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。

December 17, 2018 · 1 min · jiezi

Express使用mongodb管理会话储存 connect-mongo模块简介

简介在我的前一篇小文中express-session小书提到了express-session可以更换会话储存.那么这篇文章我们就来讲讲express在进行会话管理的时候如何将会话数据保存在外部数据库中,本文中我们使用mongodb用作会话储存数据库.本文中使用的模块以及版本号一览:模块名称版本号express4.16.4mongodb3.1.8express-session1.15.6connect-mongo2.0.3connect-mongo特性支持Express5支持所有版本的Connect支持Mongoose>=4.1.2+支持原生Mongodb驱动>=2.0.36支持Node.js 4 6 8 10支持Mongodb>=3.0事前分析由于mongodb客户端和服务器可以是多对多的关系,故有如下组合.一个客户端连接多个服务器多个客户端连接一个服务器多个客户端连接多个服务器一个客户端连接一个服务器本文主要讲解一个客户端连接一个服务器.这种情况下,一般服务器监听一个端口,而我们希望可以共享同一个mongodb驱动的实例.但是在一般情况下,我们的mongodb数据库不可能只用于会话管理任务,所以本文复用同一个连接(端口).只要复用同一个连接可以完成,那么使用单独的驱动实例来用作会话管理也就不在话下了.起步首先我们引入所有的模块:const Express = require(’express’)(), MongoClient = require(‘mongodb’).MongoClient, ExpressSession = require(’express-session’), MongoStore= require(‘connect-mongo’)(ExpressSession);看起来connect-mongo需要将express-session包装一下,这步是固定的.接下来我们定义几个常量用于连接数据库:const UrlOfDb = ‘mongodb://localhost:27017’, NameOfDb = ‘demo’, Client = new MongoClient(UrlOfDb);// 创建mongodb客户端客户端连接数据库:Client.connect((error) => { if (error) { throw error; } });使用一个数据表,并且查询几条数据: const DataBase = Client.db(NameOfDb), Collection = DataBase.collection(‘sessions’); Collection.find({}).toArray((error, result) => { if (error) { throw error; } for (const element of result) { console.log(element); } });到目前为止我们没有进行session管理,你可以替换本例中的数据表名称用于测试一下运行是否正常.完整代码:const Express = require(’express’)(), MongoClient = require(‘mongodb’).MongoClient,// 获取数据库驱动 ExpressSession = require(’express-session’),// 获取session中间件 MongoStore= require(‘connect-mongo’)(ExpressSession);// 获取session储存插件 const UrlOfDb = ‘mongodb://localhost:27017’, NameOfDb = ‘demo’, Client = new MongoClient(UrlOfDb);// 创建客户端 Client.connect((error) => { if (error) { throw error; } const DataBase = Client.db(NameOfDb),// 获取数据库 Collection = DataBase.collection(‘sessions’); // 获取数据表 // 查询数据表 Collection.find({}).toArray((error, result) => { if (error) { throw error; } for (const element of result) { console.log(element); } }); });现在我们来使用express-session中间件,并且替换掉默认的储存:// +++++const DataBase = Client.db(NameOfDb),// 获取数据库 Collection = DataBase.collection(‘sessions’),// 获取数据表 MongoStoreInstance = new MongoStore({ // 创建一个储存实例,传入db参数对于的数据库对象 db:DataBase });// 使用中间件Express.use(ExpressSession({ secret: ‘hello mongo’,// cookie签名 cookie: {maxAge: 1800000}, rolling:true, saveUninitialized:true, resave: false, store:MongoStoreInstance // 替换掉默认的储存}));// +++++++注意:connect-mongo会在该database下创建一个sessions的数据表(没有这个数据表的情况下).添加一个路由用于完成简单的验证,用于测试是否正常工作:Express.get(’/’,(request,response)=>{ if(request.session.name){ response.send(欢迎回来${request.session.name}); return ; } // 使用查询字符串当作保存的信息 request.session.name = request.query.name; request.session.pwd = request.query.pwd; response.send(欢迎登录${request.session.name});});// 启动服务器Express.listen(8888, function () { console.log(‘server is listening 8888 port!’);});完整代码:const Express = require(’express’)(), MongoClient = require(‘mongodb’).MongoClient, ExpressSession = require(’express-session’), MongoStore= require(‘connect-mongo’)(ExpressSession);const UrlOfDb = ‘mongodb://localhost:27017’, NameOfDb = ‘demo’, Client = new MongoClient(UrlOfDb);function destroyDb(Client) { return destroyDb = function () { const info = ‘Client has been closed!’; Client.close(); Client = null; console.log(info); return info; }}Client.connect((error) => { if (error) { throw error; } const DataBase = Client.db(NameOfDb), Collection = DataBase.collection(‘sessions’), MongoStoreInstance = new MongoStore({ db:DataBase }); Express.use(ExpressSession({ secret: ‘hello mongo’, cookie: {maxAge: 1800000}, rolling:true, saveUninitialized:true, resave: false, store:MongoStoreInstance })); // 使用闭包将关闭数据库挂载到全局 destroyDb(Client); // 展示复用一个连接 Collection.find({}).toArray((error, result) => { if (error) { throw error; } for (const element of result) { console.log(element); } }); Express.get(’/’,(request,response)=>{ if(request.session.name){ response.send(欢迎回来${request.session.name}); return ; } request.session.name = request.query.name; request.session.pwd = request.query.pwd; response.send(欢迎登录${request.session.name}); }); Express.get(’/closedatabase’, (request, respnose) => { respnose.send(destroyDb()); }); Express.listen(8888, function () { console.log(‘server is listening 8888 port!’); });});注意:我没有删除数据库表的常规输出,在这个例子启动的时候,你会发现他们共用了同一个连接,启动的时候会先输出数据表中的内容.测试在浏览器中输入如下内容:http://localhost:8888/?name=ascll&pwd=123456浏览器输出:欢迎登录ascll直接再次访问该页面:http://localhost:8888/浏览器输出:欢迎回来ascll此时在数据库中手动查询后,或者重启本项目,你会在控制台中发现上次留下的session记录:{ _id: ‘qbP36wE0nJkvtyNqx_6Amoesjjcsr-sD’, expires: 2018-12-14T08:27:19.809Z, session: ‘{“cookie”:{“originalMaxAge”:1800000,“expires”:“2018-12-14T08:20:21.519Z”,“httpOnly”:true,“path”:"/"},“name”:“ascll”,“pwd”:“123456”}’ }使用总结引入connect-mongo和express-session然后调用connect-mongo将express-sessino传入获取上一步返回的类,然后使用express-session中间件的时候对于store选传入这个类的实例对象api创建Express 4.x, 5.0 and Connect 3.x:const session = require(’express-session’);const MongoStore = require(‘connect-mongo’)(session); app.use(session({ secret: ‘foo’, store: new MongoStore(options)}));Express 2.x, 3.x and Connect 1.x, 2.x:const MongoStore = require(‘connect-mongo’)(express); app.use(express.session({ secret: ‘foo’, store: new MongoStore(options)}));连接到MongoDb使用mongooseconst mongoose = require(‘mongoose’); // 基本使用mongoose.connect(connectionOptions); app.use(session({ store: new MongoStore({ mongooseConnection: mongoose.connection })})); // 建议使用方式,这样可以复用连接const connection = mongoose.createConnection(connectionOptions); app.use(session({ store: new MongoStore({ mongooseConnection: connection })}));使用Mongo原生Node驱动这种情况下你需要将一个mongodb驱动的一个数据库实例传递给connect-mongo.如果数据库没有打开connect-mongo会自动帮你连接./* 这里有很多种方式来获取一个数据库实例,具体可以参考官网文档.*/app.use(session({ store: new MongoStore({ db: dbInstance }) // 别忘了MongoStore是connect-mongo传入express-session后返回的一个函数}));// 或者也可以使用Promise版本app.use(session({ store: new MongoStore({ dbPromise: dbInstancePromise })}));通过连接字符串创建一个连接// Basic usageapp.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’ })})); // Advanced usageapp.use(session({ store: new MongoStore({ url: ‘mongodb://user12345:foobar@localhost/test-app?authSource=admins&w=1’, mongoOptions: advancedOptions // See below for details })}));事件一个MongoStore实例有如下的事件:事件名称描述回调参数createsession创建后触发sessionIdtouchsession被获取但是未修改sessionIdupdatesession被更新sessionIdsetsession创建后或者更新后(为了兼容)sessionIddestroysession被销毁后sessionId使用我们之前的例子中添加如下的代码:// +++MongoStoreInstance.on(‘create’,(sessionId)=>{ console.log(‘create’,sessionId);});MongoStoreInstance.on(’touch’,(sessionId)=>{ console.log(‘create’, sessionId);});MongoStoreInstance.on(‘update’,(sessionId)=>{ console.log(‘update’, sessionId);});MongoStoreInstance.on(‘set’,(sessionId)=>{ console.log(‘set’, sessionId);});MongoStoreInstance.on(‘destroy’,(sessionId)=>{ console.log(‘destroy’, sessionId);});// +++清空cookie后再次运行服务器,多执行几个操作你就可以看到session的创建以及修改等操作.session过期处理基本处理方式connect-mongo只会使用配置了过期时间的cookie,如果没有设置则会创建一个新的cookie并且使用tll选项来指定过期时间:app.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’, ttl: 14 * 24 * 60 * 60 // 默认过期时间为14天 })}));注意:用户的每次访问都会刷新过期时间.删除过期session默认情况下connect-mongo使用MongoDB’s TTL collection特性(2.2+)用于自动的移出过期的session.但是你可以修改这种行为.connect-mongo会在开始的时候创建一个TTl索引,前提是你的Mongo db版本在(2.2+)且有权限执行这一操作.app.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’, autoRemove: ’native’ // Default })}));注意:这种默认的行为不适用于高并发的情况,这种情况下你需要禁用默认模式,然后自行定义TTl索引.使用兼容模式如果你使用了Mongodb的老版本或者不希望创建TTL索引,你可以指定一个间隔时间让connect-mongo来删除这些过期的session.app.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’, autoRemove: ‘interval’, autoRemoveInterval: 10 // 单位分钟 })}));禁用过期session删除app.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’, autoRemove: ‘disabled’ })}));session懒更新如果你使用的express-session版本>=1.10,然后不希望用户每次浏览页面的时候或刷新页面的时候都要重新保存,你可以限制一段时间内更新session.app.use(express.session({ secret: ‘keyboard cat’, saveUninitialized: false, // 如果不保存则不会创建session resave: false, // 如果未修改则不会保存 store: new MongoStore({ url: ‘mongodb://localhost/test-app’, touchAfter: 24 * 3600 // 指定触发间隔时间 单位秒 })}));通过这样设置session只会在24小时内触发1次无论用户浏览多少次页面或者刷新多少次.修改session除外.其他选项collection 指定缓存数据表的名字默认sessionsfallbackMemory 回退处理默认使用MemoryStore进行存储stringify 默认是true,如果为true则序列化和反序列化使用原生的JSON.xxx处理.serialize 自定义序列化函数unserialize 自定义反序列化函数transformId 将sessionId转为你想要的任何键然后进行储存暗坑也不算是暗坑吧,一用有两点:Mongodb客户端正常关闭后connect-mongo会报错,虽然会被Express拦截但是这个模块没有提供error事件.Express中间件必须同步挂载?在我的例子中尝试异步加载express-session中间件,但是失败了中间件没有效果.connect-mongo模块npm地址https://www.npmjs.com/package… ...

December 14, 2018 · 3 min · jiezi

BUG总结 - 用户反馈项目(vue+express+hybird)

项目描述:类似淘宝客服聊天界面和功能,用于用户反馈问题和运营推送优质内容/版本更新给用户1.弹出输入框的同时自动弹出键盘解决:让textarea获取焦点,调用focus()方法就会自动调起键盘this.$nextTick(() => { this.$refs.textarea.focus()})关键点:1.代码顺序,此行代码必须在设置输入框显示(this.showComment = true)后执行2.vue@2.5.11此处,在ios时键盘不会自动吊起,换成vue@2.4.4可以吊起,官方有issue,不过没写解决方法2.将vue版本降到@2.4.4后,再次运行报错 同时构建项目出错解决:vue的版本要与vue-template-compiler版本一致,一起降为2.4.43.所有接口用postman请求一直返回index.html页面解决:注意代码顺序app.use(’/api’, fetchUserInfo, apiRoutes) // 接口在history的上面app.use(history()) // 这句代码需要在express.static上面app.use(express.static(path.join(__dirname, ‘../../dist’)))原因:待4.本地调试正确,测试环境异常,发现new Date()时间比本地早八个小时原因: 项目因为是用了最新的vue-cli3构建,所需要的node版本较高,隔离项目放入docker中,docker的默认时区是UTC,应改为CST解决:// dockerfileRUN rm /etc/localtime \ && export DEBIAN_FRONTEND=noninteractive \ && apt-get install -y tzdata \ && ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime5.调用bridge发送图片信息,交互体验差,低网速环境下,发送失败几率大现状:等到图片上传到七牛,拿到图片网络地址后再渲染页面和发送消息,期间没有任何提示解决:调用bridge,从相册/相机选择照片后–>客户端返回base64的图片信息–>把图片消息先渲染到页面上,上面加蒙层loading–>等待上传图片的bridge方法返回七牛地址,成功拿到则蒙层消失,失败则提示重新发送,点击重新发送,则重新上传图片

December 13, 2018 · 1 min · jiezi

十大热门的JavaScript框架和库

JavaScript 框架和库可以说是开源项目中最庞大也是最累的类目了,目前在github 上这一类的项目是最多的,并且几乎每隔一段时间就会出现一个新的项目席卷网络社区,虽然这样推动了创新的发展,但不得不说苦了前端的开发者们。因此本文罗列出了一些优秀的 Javascript 框架和库的特及其在 github 上的 star 数,旨在为各位开发者提供一些参考。1、ReactJS(Star: 59989,Fork: 10992)主页:了解更多React.js(React)是一个用来构建用户界面的 JavaScript 库,主要用于构建UI,很多人认为 React 是 MVC 中的 V(视图)。React 起源于 Facebook 的内部项目,用来架设 Instagram 的网站,并于 2013 年 5 月开源。React 拥有较高的性能,代码逻辑非常简单,越来越多的人已开始关注和使用它。React 特点: 1.声明式设计−React采用声明范式,可以轻松描述应用。 2.高效−React通过对DOM的模拟,最大限度地减少与DOM的交互。 3.灵活−React可以与已知的库或框架很好地配合。 4.JSX− JSX 是 JavaScript 语法的扩展。React 开发不一定使用 JSX ,但我们建议使用它。 5.组件− 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。 6.单向响应的数据流− React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。2、AngularJS(Star: 54769,Fork: 27292)主页:https://angularjs.orgAngular JS (Angular.JS) 是一组用来开发 Web 页面的框架、模板以及数据绑定和丰富 UI 组件。它支持整个开发进程,提供 Web 应用的架构,无需进行手工 DOM 操作。 AngularJS 很小,只有 60K,兼容主流浏览器,与 jQuery 配合良好。3、Vue.js(Star: 43608, Fork: 5493)https://cn.vuejs.org/Vue.js 是构建 Web 界面的 JavaScript 库,提供数据驱动的组件,还有简单灵活的 API,使得 MVVM 更简单。主要特性: ●可扩展的数据绑定 ●将普通的 JS 对象作为 model ●简洁明了的 API ●组件化 UI 构建 ●配合别的库使用4、jQuery(Star: 43432, Fork: 12117)主页:https://jquery.com/JQuery 是轻量级的js库(压缩后只有21k) ,它兼容CSS3,还兼容各种浏览器 (IE 6.0+, FF 1.5+, Safari 2.0+, Opera 9.0+)。jQuery使用户能更方便地处理HTML documents、events、实现动画效果,并且方便地为网站提供AJAX交互。jQuery还有一个比较大的优势是,它的文档说明很全,而且各种 应用也说得很详细,同时还有许多成熟的插件可供选择。jQuery能够使用户的html页保持代码和html内容分离,也就是说,不用再在html里面插入一堆js来调用命令了,只需定义id即可。5、Meteor(Star: 36691,Fork: 4617)主页:http://www.meteor.comMeteor 是一组新的技术用于构建高质量的 Web 应用,提供很多现成的包,可直接在浏览器或者云平台中运行。6、Angular2(Star:20803,Fork:5367)主页:https://angular.ioAngular 是一款十分流行且好用的 Web 前端框架,目前由 Google 维护。这个条目收录的是 Angular 2 及其后面的版本。由于官方已将 Angular 2 和之前的版本Angular.js分开维护(两者的 GitHub 地址和项目主页皆不相同),所以就有了这个页面。7、Ember.js(Star: 17540,Fork: 3646)主页:http://emberjs.comEmber是一个雄心勃勃的Web应用程序,消除了样板,并提供了一个标准的应用程序架构的JavaScript框架。8、Polymer(Star:16979,Fork: 1699)主页:http://www.polymer-project.org在2013年的Google I/O大会上,Google发布了Polymer,它是一个使用Web组件构建Web应用的类库,同时也使用了为Web构建可重用组件的新的HTML 5标准。Polymer为大部分Web组件技术提供了polyfills功能,它能让开发者在所有的浏览器支持新特性前创建自己的可重用组件。此外,Polymer提供了一系列的部件的例子,其中包括天气、时钟、股票行情和线型图。Polymer中的polyfills为需要使用Web组件成功构建应用提供了多种Web技术,包括: ●HTML imports:种在其他HTML document中引入和重用HTML document的方法。 ●自定义元素:让开发者定义和使用自定义DOM元素。 ●Shadow DOM:在DOM中提供的封装。 ●模型驱动视图(Model Driven Views):提供象AngularJS的数据绑定。 ●Web动画:实现复杂动画的API。 ●Pointer事件:对鼠标触摸和手写笔事件的封装9、Zepto.js(Star: 12074,Fork: 3260)主页:https://facebook.github.io/react Zepto.js 是支持移动WebKit浏览器的JavaScript框架,具有与jQuery兼容的语法。2-5k的库,通过不错的API处理绝大多数的基本工作。10、Riot.js(Star: 11491,Fork: 902)主页:http://riotjs.comRiot.js是一个客户端模型-视图-呈现(MVP)框架并且它非常轻量级甚至小于1kb.尽管他的大小令人难以置信,所有它能构建的有如下:一个模板引擎,路由,甚至是库和一个严格的并具有组织的MVP模式。当模型数据变化时视图也会自动更新。当然除了以上提到的这些,还有很多优秀的 Javascript 框架和库,并且几乎每隔一段时间就会涌现一个新的产品。 ...

December 9, 2018 · 1 min · jiezi

后端小白的我,是如何成功搭建 express+mongodb 的简洁博客网站后端的

前言blog-node 是采用了主流的前后端分离思想的,主里只讲 后端。blog-node 项目是 node + express + mongodb 的进行开发的,项目已经开源,项目地址在 github 上。效果请看 http://乐趣区.cn/main.html1. 后端1.1 已经实现功能[x] 登录[x] 文章管理[x] 标签管理[x] 评论[x] 留言管理[x] 用户管理[x] 友情链接管理[x] 时间轴管理[x] 身份验证1.2 待实现功能[ ] 点赞、留言和评论 的通知管理[ ] 个人中心(用来设置博主的各种信息)[ ] 工作台( 接入百度统计接口,查看网站浏览量和用户访问等数据 )2. 技术nodecookie-parser : “~1.4.3"crypto : “^1.0.1"express: “~4.16.0"express-session : “^1.15.6”,http-errors : “~1.6.2”,mongodb : “^3.1.8”,mongoose : “^5.3.7”,mongoose-auto-increment : “^5.0.1”,yargs : “^12.0.2"3. 主文件 app.js// modulesconst createError = require(‘http-errors’);const express = require(’express’);const path = require(‘path’);const cookieParser = require(‘cookie-parser’);const logger = require(‘morgan’);const session = require(’express-session’);// import 等语法要用到 babel 支持require(‘babel-register’);const app = express();// view engine setupapp.set(‘views’, path.join(__dirname, ‘views’));app.set(‘view engine’, ’ejs’);app.use(logger(‘dev’));app.use(express.json());app.use(express.urlencoded({ extended: false }));app.use(express.static(path.join(__dirname, ‘public’)));app.use(cookieParser(‘blog_node_cookie’));app.use( session({ secret: ‘blog_node_cookie’, name: ‘session_id’, //# 在浏览器中生成cookie的名称key,默认是connect.sid resave: true, saveUninitialized: true, cookie: { maxAge: 60 * 1000 * 30, httpOnly: true }, //过期时间 }),);const mongodb = require(’./core/mongodb’);// data servermongodb.connect();//将路由文件引入const route = require(’./routes/index’);//初始化所有路由route(app);// catch 404 and forward to error handlerapp.use(function(req, res, next) { next(createError(404));});// error handlerapp.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get(’env’) === ‘development’ ? err : {}; // render the error page res.status(err.status || 500); res.render(’error’);});module.exports = app;4. 数据库 core/mongodb.js/** * Mongoose module. * @file 数据库模块 * @module core/mongoose * @author 乐趣区 <https://github.com/乐趣区> /const consola = require(‘consola’)const CONFIG = require(’../app.config.js’)const mongoose = require(‘mongoose’)const autoIncrement = require(‘mongoose-auto-increment’)// remove DeprecationWarningmongoose.set(‘useFindAndModify’, false)// mongoose Promisemongoose.Promise = global.Promise// mongooseexports.mongoose = mongoose// connectexports.connect = () => { // 连接数据库 mongoose.connect(CONFIG.MONGODB.uri, { useCreateIndex: true, useNewUrlParser: true, promiseLibrary: global.Promise }) // 连接错误 mongoose.connection.on(’error’, error => { consola.warn(‘数据库连接失败!’, error) }) // 连接成功 mongoose.connection.once(‘open’, () => { consola.ready(‘数据库连接成功!’) }) // 自增 ID 初始化 autoIncrement.initialize(mongoose.connection) // 返回实例 return mongoose}5. 数据模型 Model这里只介绍 用户、文章和评论 的模型。5.1 用户用户的字段都有设置类型 type,大多都设置了默认值 default ,邮箱设置了验证规则 validate,密码保存用了 crypto 来加密。用了中间件自增 ID 插件 mongoose-auto-increment。/* * User model module. * @file 权限和用户数据模型 * @module model/user * @author 乐趣区 <https://github.com/乐趣区> /const crypto = require(‘crypto’);const { argv } = require(‘yargs’);const { mongoose } = require(’../core/mongodb.js’);const autoIncrement = require(‘mongoose-auto-increment’);const adminSchema = new mongoose.Schema({ // 名字 name: { type: String, required: true, default: ’’ }, // 用户类型 0:博主 1:其他用户 type: { type: Number, default: 1 }, // 手机 phone: { type: String, default: ’’ }, //封面 img_url: { type: String, default: ’’ }, // 邮箱 email: { type: String, required: true, validate: /\w[-\w.+]@([A-Za-z0-9][-A-Za-z0-9]+.)+[A-Za-z]{2,14}/ }, // 个人介绍 introduce: { type: String, default: ’’ }, // 头像 avatar: { type: String, default: ‘user’ }, // 密码 password: { type: String, required: true, default: crypto .createHash(‘md5’) .update(argv.auth_default_password || ‘root’) .digest(‘hex’), }, // 创建日期 create_time: { type: Date, default: Date.now }, // 最后修改日期 update_time: { type: Date, default: Date.now },});// 自增 ID 插件配置adminSchema.plugin(autoIncrement.plugin, { model: ‘User’, field: ‘id’, startAt: 1, incrementBy: 1,});module.exports = mongoose.model(‘User’, adminSchema);5.2 文章文章是分类型的:文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍而且简历和管理员介绍的文章只能是各自一篇(因为前台展示那里有个导航 关于我 ,就是请求管理员介绍这篇文章的,简历也是打算这样子用的),普通文章可以是无数篇。点赞的用户 like_users 那里应该只保存用户 id 的,这个后面修改一下。/** * Article model module. * @file 文章数据模型 * @module model/article * @author 乐趣区 <https://github.com/乐趣区> /const { mongoose } = require(’../core/mongodb.js’);const autoIncrement = require(‘mongoose-auto-increment’);// 文章模型const articleSchema = new mongoose.Schema({ // 文章标题 title: { type: String, required: true, validate: /\S+/ }, // 文章关键字(SEO) keyword: [{ type: String, default: ’’ }], // 作者 author: { type: String, required: true, validate: /\S+/ }, // 文章描述 desc: { type: String, default: ’’ }, // 文章内容 content: { type: String, required: true, validate: /\S+/ }, // 字数 numbers: { type: String, default: 0 }, // 封面图 img_url: { type: String, default: ‘https://upload-images.jianshu.io/upload_images/12890819-80fa7517ab3f2783.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240' }, // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍 type: { type: Number, default: 1 }, // 文章发布状态 => 0 草稿,1 已发布 state: { type: Number, default: 1 }, // 文章转载状态 => 0 原创,1 转载,2 混合 origin: { type: Number, default: 0 }, // 文章标签 tags: [{ type: mongoose.Schema.Types.ObjectId, ref: ‘Tag’, required: true }], comments: [{ type: mongoose.Schema.Types.ObjectId, ref: ‘Comment’, required: true }], // 文章分类 category: [{ type: mongoose.Schema.Types.ObjectId, ref: ‘Category’, required: true }], // 点赞的用户 like_users: [ { // 用户id id: { type: mongoose.Schema.Types.ObjectId }, // 名字 name: { type: String, required: true, default: ’’ }, // 用户类型 0:博主 1:其他用户 type: { type: Number, default: 1 }, // 个人介绍 introduce: { type: String, default: ’’ }, // 头像 avatar: { type: String, default: ‘user’ }, // 创建日期 create_time: { type: Date, default: Date.now }, }, ], // 其他元信息 meta: { views: { type: Number, default: 0 }, likes: { type: Number, default: 0 }, comments: { type: Number, default: 0 }, }, // 创建日期 create_time: { type: Date, default: Date.now }, // 最后修改日期 update_time: { type: Date, default: Date.now },});// 自增 ID 插件配置articleSchema.plugin(autoIncrement.plugin, { model: ‘Article’, field: ‘id’, startAt: 1, incrementBy: 1,});// 文章模型module.exports = mongoose.model(‘Article’, articleSchema);5.3 评论评论功能是实现了简单的三级评论的,第三者的评论(就是别人对一级评论进行再评论)放在 other_comments 里面。/* * Comment model module. * @file 评论数据模型 * @module model/comment * @author 乐趣区 <https://github.com/乐趣区> */const { mongoose } = require(’../core/mongodb.js’);const autoIncrement = require(‘mongoose-auto-increment’);// 评论模型const commentSchema = new mongoose.Schema({ // 评论所在的文章 id article_id: { type: mongoose.Schema.Types.ObjectId, required: true }, // content content: { type: String, required: true, validate: /\S+/ }, // 是否置顶 is_top: { type: Boolean, default: false }, // 被赞数 likes: { type: Number, default: 0 }, user_id: { type: mongoose.Schema.Types.ObjectId, ref: ‘User’, required: true }, // 父评论的用户信息 user: { // 用户id user_id: { type: mongoose.Schema.Types.ObjectId }, // 名字 name: { type: String, required: true, default: ’’ }, // 用户类型 0:博主 1:其他用户 type: { type: Number, default: 1 }, // 头像 avatar: { type: String, default: ‘user’ }, }, // 第三者评论 other_comments: [ { user: { id: { type: mongoose.Schema.Types.ObjectId }, // 名字 name: { type: String, required: true, default: ’’ }, // 用户类型 0:博主 1:其他用户 type: { type: Number, default: 1 }, }, // content content: { type: String, required: true, validate: /\S+/ }, // 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论 state: { type: Number, default: 1 }, // 创建日期 create_time: { type: Date, default: Date.now }, }, ], // 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论 state: { type: Number, default: 1 }, // 创建日期 create_time: { type: Date, default: Date.now }, // 最后修改日期 update_time: { type: Date, default: Date.now },});// 自增 ID 插件配置commentSchema.plugin(autoIncrement.plugin, { model: ‘Comment’, field: ‘id’, startAt: 1, incrementBy: 1,});// 标签模型module.exports = mongoose.model(‘Comment’, commentSchema);其他模块的具体需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。6. 路由接口 routes6.1 主文件/*所有的路由接口/const user = require(’./user’);const article = require(’./article’);const comment = require(’./comment’);const message = require(’./message’);const tag = require(’./tag’);const link = require(’./link’);const category = require(’./category’);const timeAxis = require(’./timeAxis’);module.exports = app => { app.post(’/login’, user.login); app.post(’/logout’, user.logout); app.post(’/loginAdmin’, user.loginAdmin); app.post(’/register’, user.register); app.post(’/delUser’, user.delUser); app.get(’/currentUser’, user.currentUser); app.get(’/getUserList’, user.getUserList); app.post(’/addComment’, comment.addComment); app.post(’/addThirdComment’, comment.addThirdComment); app.post(’/changeComment’, comment.changeComment); app.post(’/changeThirdComment’, comment.changeThirdComment); app.get(’/getCommentList’, comment.getCommentList); app.post(’/addArticle’, article.addArticle); app.post(’/updateArticle’, article.updateArticle); app.post(’/delArticle’, article.delArticle); app.get(’/getArticleList’, article.getArticleList); app.get(’/getArticleListAdmin’, article.getArticleListAdmin); app.post(’/getArticleDetail’, article.getArticleDetail); app.post(’/likeArticle’, article.likeArticle); app.post(’/addTag’, tag.addTag); app.post(’/delTag’, tag.delTag); app.get(’/getTagList’, tag.getTagList); app.post(’/addMessage’, message.addMessage); app.post(’/addReplyMessage’, message.addReplyMessage); app.post(’/delMessage’, message.delMessage); app.post(’/getMessageDetail’, message.getMessageDetail); app.get(’/getMessageList’, message.getMessageList); app.post(’/addLink’, link.addLink); app.post(’/updateLink’, link.updateLink); app.post(’/delLink’, link.delLink); app.get(’/getLinkList’, link.getLinkList); app.post(’/addCategory’, category.addCategory); app.post(’/delCategory’, category.delCategory); app.get(’/getCategoryList’, category.getCategoryList); app.post(’/addTimeAxis’, timeAxis.addTimeAxis); app.post(’/updateTimeAxis’, timeAxis.updateTimeAxis); app.post(’/delTimeAxis’, timeAxis.delTimeAxis); app.get(’/getTimeAxisList’, timeAxis.getTimeAxisList); app.post(’/getTimeAxisDetail’, timeAxis.getTimeAxisDetail);};6.2 文章各模块的列表都是用了分页的形式的。import Article from ‘../models/article’;import User from ‘../models/user’;import { responseClient, timestampToTime } from ‘../util/util’;exports.addArticle = (req, res) => { // if (!req.session.userInfo) { // responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); // return; // } const { title, author, keyword, content, desc, img_url, tags, category, state, type, origin } = req.body; let tempArticle = null if(img_url){ tempArticle = new Article({ title, author, keyword: keyword ? keyword.split(’,’) : [], content, numbers: content.length, desc, img_url, tags: tags ? tags.split(’,’) : [], category: category ? category.split(’,’) : [], state, type, origin, }); }else{ tempArticle = new Article({ title, author, keyword: keyword ? keyword.split(’,’) : [], content, numbers: content.length, desc, tags: tags ? tags.split(’,’) : [], category: category ? category.split(’,’) : [], state, type, origin, }); } tempArticle .save() .then(data => { responseClient(res, 200, 0, ‘保存成功’, data); }) .catch(err => { console.log(err); responseClient(res); });};exports.updateArticle = (req, res) => { // if (!req.session.userInfo) { // responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); // return; // } const { title, author, keyword, content, desc, img_url, tags, category, state, type, origin, id } = req.body; Article.update( { _id: id }, { title, author, keyword: keyword ? keyword.split(’,’): [], content, desc, img_url, tags: tags ? tags.split(’,’) : [], category:category ? category.split(’,’) : [], state, type, origin, }, ) .then(result => { responseClient(res, 200, 0, ‘操作成功’, result); }) .catch(err => { console.error(err); responseClient(res); });};exports.delArticle = (req, res) => { let { id } = req.body; Article.deleteMany({ _id: id }) .then(result => { if (result.n === 1) { responseClient(res, 200, 0, ‘删除成功!’); } else { responseClient(res, 200, 1, ‘文章不存在’); } }) .catch(err => { console.error(’err :’, err); responseClient(res); });};// 前台文章列表exports.getArticleList = (req, res) => { let keyword = req.query.keyword || null; let state = req.query.state || ‘’; let likes = req.query.likes || ‘’; let tag_id = req.query.tag_id || ‘’; let category_id = req.query.category_id || ‘’; let pageNum = parseInt(req.query.pageNum) || 1; let pageSize = parseInt(req.query.pageSize) || 10; let conditions = {}; if (!state) { if (keyword) { const reg = new RegExp(keyword, ‘i’); //不区分大小写 conditions = { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }], }; } } else if (state) { state = parseInt(state); if (keyword) { const reg = new RegExp(keyword, ‘i’); conditions = { $and: [ { $or: [{ state: state }] }, { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }, { keyword: { $regex: reg } }] }, ], }; } else { conditions = { state }; } } let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize; let responseData = { count: 0, list: [], }; Article.countDocuments(conditions, (err, count) => { if (err) { console.log(‘Error:’ + err); } else { responseData.count = count; // 待返回的字段 let fields = { title: 1, author: 1, keyword: 1, content: 1, desc: 1, img_url: 1, tags: 1, category: 1, state: 1, type: 1, origin: 1, comments: 1, like_User_id: 1, meta: 1, create_time: 1, update_time: 1, }; let options = { skip: skip, limit: pageSize, sort: { create_time: -1 }, }; Article.find(conditions, fields, options, (error, result) => { if (err) { console.error(‘Error:’ + error); // throw error; } else { let newList = []; if (likes) { // 根据热度 likes 返回数据 result.sort((a, b) => { return b.meta.likes - a.meta.likes; }); responseData.list = result; } else if (category_id) { // 根据 分类 id 返回数据 result.forEach(item => { if (item.category.indexOf(category_id) > -1) { newList.push(item); } }); let len = newList.length; responseData.count = len; responseData.list = newList; } else if (tag_id) { // 根据标签 id 返回数据 result.forEach(item => { if (item.tags.indexOf(tag_id) > -1) { newList.push(item); } }); let len = newList.length; responseData.count = len; responseData.list = newList; } else { responseData.list = result; } responseClient(res, 200, 0, ‘操作成功!’, responseData); } }); } });};// 后台文章列表exports.getArticleListAdmin = (req, res) => { let keyword = req.query.keyword || null; let state = req.query.state || ‘’; let likes = req.query.likes || ‘’; let pageNum = parseInt(req.query.pageNum) || 1; let pageSize = parseInt(req.query.pageSize) || 10; let conditions = {}; if (!state) { if (keyword) { const reg = new RegExp(keyword, ‘i’); //不区分大小写 conditions = { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }], }; } } else if (state) { state = parseInt(state); if (keyword) { const reg = new RegExp(keyword, ‘i’); conditions = { $and: [ { $or: [{ state: state }] }, { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }, { keyword: { $regex: reg } }] }, ], }; } else { conditions = { state }; } } let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize; let responseData = { count: 0, list: [], }; Article.countDocuments(conditions, (err, count) => { if (err) { console.log(‘Error:’ + err); } else { responseData.count = count; // 待返回的字段 let fields = { title: 1, author: 1, keyword: 1, content: 1, desc: 1, img_url: 1, tags: 1, category: 1, state: 1, type: 1, origin: 1, comments: 1, like_User_id: 1, meta: 1, create_time: 1, update_time: 1, }; let options = { skip: skip, limit: pageSize, sort: { create_time: -1 }, }; Article.find(conditions, fields, options, (error, result) => { if (err) { console.error(‘Error:’ + error); // throw error; } else { if (likes) { result.sort((a, b) => { return b.meta.likes - a.meta.likes; }); } responseData.list = result; responseClient(res, 200, 0, ‘操作成功!’, responseData); } }) .populate([ { path: ’tags’, }, { path: ‘comments’, }, { path: ‘category’, }, ]) .exec((err, doc) => {}); } });};// 文章点赞exports.likeArticle = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { id, user_id } = req.body; Article.findOne({ _id: id }) .then(data => { let fields = {}; data.meta.likes = data.meta.likes + 1; fields.meta = data.meta; let like_users_arr = data.like_users.length ? data.like_users : []; User.findOne({ _id: user_id }) .then(user => { let new_like_user = {}; new_like_user.id = user._id; new_like_user.name = user.name; new_like_user.avatar = user.avatar; new_like_user.create_time = user.create_time; new_like_user.type = user.type; new_like_user.introduce = user.introduce; like_users_arr.push(new_like_user); fields.like_users = like_users_arr; Article.update({ _id: id }, fields) .then(result => { responseClient(res, 200, 0, ‘操作成功!’, result); }) .catch(err => { console.error(’err :’, err); throw err; }); }) .catch(err => { responseClient(res); console.error(’err 1:’, err); }); }) .catch(err => { responseClient(res); console.error(’err 2:’, err); });};// 文章详情exports.getArticleDetailByType = (req, res) => { let { type } = req.body; if (!type) { responseClient(res, 200, 1, ‘文章不存在 !’); return; } Article.findOne({ type: type }, (Error, data) => { if (Error) { console.error(‘Error:’ + Error); // throw error; } else { data.meta.views = data.meta.views + 1; Article.updateOne({ type: type }, { meta: data.meta }) .then(result => { responseClient(res, 200, 0, ‘操作成功 !’, data); }) .catch(err => { console.error(’err :’, err); throw err; }); } }) .populate([ { path: ’tags’, select: ‘-_id’ }, { path: ‘category’, select: ‘-_id’ }, { path: ‘comments’, select: ‘-_id’ }, ]) .exec((err, doc) => { // console.log(“doc:”); // aikin // console.log(“doc.tags:",doc.tags); // aikin // console.log(“doc.category:",doc.category); // undefined });};// 文章详情exports.getArticleDetail = (req, res) => { let { id } = req.body; let type = Number(req.body.type) || 1; //文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍 console.log(’type:’, type); if (type === 1) { if (!id) { responseClient(res, 200, 1, ‘文章不存在 !’); return; } Article.findOne({ _id: id }, (Error, data) => { if (Error) { console.error(‘Error:’ + Error); // throw error; } else { data.meta.views = data.meta.views + 1; Article.updateOne({ _id: id }, { meta: data.meta }) .then(result => { responseClient(res, 200, 0, ‘操作成功 !’, data); }) .catch(err => { console.error(’err :’, err); throw err; }); } }) .populate([ { path: ’tags’, }, { path: ‘category’, }, { path: ‘comments’, }, ]) .exec((err, doc) => { // console.log(“doc:”); // aikin // console.log(“doc.tags:",doc.tags); // aikin // console.log(“doc.category:",doc.category); // undefined }); } else { Article.findOne({ type: type }, (Error, data) => { if (Error) { console.log(‘Error:’ + Error); // throw error; } else { if (data) { data.meta.views = data.meta.views + 1; Article.updateOne({ type: type }, { meta: data.meta }) .then(result => { responseClient(res, 200, 0, ‘操作成功 !’, data); }) .catch(err => { console.error(’err :’, err); throw err; }); } else { responseClient(res, 200, 1, ‘文章不存在 !’); return; } } }) .populate([ { path: ’tags’, }, { path: ‘category’, }, { path: ‘comments’, }, ]) .exec((err, doc) => {}); }};6.3 评论评论是有状态的:状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论。管理一级和三级评论是设置前台能不能展示的,默认是展示,如果管理员看了,是条垃圾评论就 设置为 -1 或者 -2 ,进行隐藏,前台就不会展现了。import { responseClient } from ‘../util/util’;import Comment from ‘../models/comment’;import User from ‘../models/user’;import Article from ‘../models/article’;//获取全部评论exports.getCommentList = (req, res) => { let keyword = req.query.keyword || null; let comment_id = req.query.comment_id || null; let pageNum = parseInt(req.query.pageNum) || 1; let pageSize = parseInt(req.query.pageSize) || 10; let conditions = {}; if (comment_id) { if (keyword) { const reg = new RegExp(keyword, ‘i’); //不区分大小写 conditions = { _id: comment_id, content: { $regex: reg }, }; } else { conditions = { _id: comment_id, }; } } else { if (keyword) { const reg = new RegExp(keyword, ‘i’); //不区分大小写 conditions = { content: { $regex: reg }, }; } } let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize; let responseData = { count: 0, list: [], }; Comment.countDocuments(conditions, (err, count) => { if (err) { console.error(‘Error:’ + err); } else { responseData.count = count; // 待返回的字段 let fields = { article_id: 1, content: 1, is_top: 1, likes: 1, user_id: 1, user: 1, other_comments: 1, state: 1, create_time: 1, update_time: 1, }; let options = { skip: skip, limit: pageSize, sort: { create_time: -1 }, }; Comment.find(conditions, fields, options, (error, result) => { if (err) { console.error(‘Error:’ + error); // throw error; } else { responseData.list = result; responseClient(res, 200, 0, ‘操作成功!’, responseData); } }); } });};// 添加一级评论exports.addComment = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { article_id, user_id, content } = req.body; User.findById({ _id: user_id, }) .then(result => { // console.log(‘result :’, result); if (result) { let userInfo = { user_id: result._id, name: result.name, type: result.type, avatar: result.avatar, }; let comment = new Comment({ article_id: article_id, content: content, user_id: user_id, user: userInfo, }); comment .save() .then(commentResult => { Article.findOne({ _id: article_id }, (errors, data) => { if (errors) { console.error(‘Error:’ + errors); // throw errors; } else { data.comments.push(commentResult._id); data.meta.comments = data.meta.comments + 1; Article.updateOne({ _id: article_id }, { comments: data.comments, meta: data.meta }) .then(result => { responseClient(res, 200, 0, ‘操作成功 !’, commentResult); }) .catch(err => { console.error(’err :’, err); throw err; }); } }); }) .catch(err2 => { console.error(’err :’, err2); throw err2; }); } else { responseClient(res, 200, 1, ‘用户不存在’); } }) .catch(error => { console.error(’error :’, error); responseClient(res); });};// 添加第三者评论exports.addThirdComment = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { article_id, comment_id, user_id, content } = req.body; Comment.findById({ _id: comment_id, }) .then(commentResult => { User.findById({ _id: user_id, }) .then(userResult => { if (userResult) { let userInfo = { user_id: userResult._id, name: userResult.name, type: userResult.type, avatar: userResult.avatar, }; let item = { user: userInfo, content: content, }; commentResult.other_comments.push(item); Comment.updateOne( { _id: comment_id }, { other_comments: commentResult, }, ) .then(result => { responseClient(res, 200, 0, ‘操作成功’, result); Article.findOne({ _id: article_id }, (errors, data) => { if (errors) { console.error(‘Error:’ + errors); // throw errors; } else { data.meta.comments = data.meta.comments + 1; Article.updateOne({ _id: article_id }, { meta: data.meta }) .then(result => { // console.log(‘result :’, result); responseClient(res, 200, 0, ‘操作成功 !’, result); }) .catch(err => { console.log(’err :’, err); throw err; }); } }); }) .catch(err1 => { console.error(’err1:’, err1); responseClient(res); }); } else { responseClient(res, 200, 1, ‘用户不存在’); } }) .catch(error => { console.error(’error :’, error); responseClient(res); }); }) .catch(error2 => { console.error(’error2 :’, error2); responseClient(res); });};// 管理一级评论exports.changeComment = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { id, state } = req.body; Comment.updateOne( { _id: id }, { state: Number(state), }, ) .then(result => { responseClient(res, 200, 0, ‘操作成功’, result); }) .catch(err => { console.error(’err:’, err); responseClient(res); });};// 管理第三者评论exports.changeThirdComment = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { comment_id, state, index } = req.body; Comment.findById({ _id: comment_id, }) .then(commentResult => { let i = index ? Number(index) : 0; if (commentResult.other_comments.length) { commentResult.other_comments[i].state = Number(state); Comment.updateOne( { _id: comment_id }, { other_comments: commentResult, }, ) .then(result => { responseClient(res, 200, 0, ‘操作成功’, result); }) .catch(err1 => { console.error(’err1:’, err1); responseClient(res); }); } else { responseClient(res, 200, 1, ‘第三方评论不存在!’, result); } }) .catch(error2 => { console.log(’error2 :’, error2); responseClient(res); });};其他模块的具体需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。7. Build Setup ( 构建安装 )# install dependenciesnpm install # serve with hot reload at localhost: 3000npm start # build for production with minification请使用 pm2 ,可以永久运行在服务器上,且不会一报错 node 程序就挂了。8. 项目地址如果觉得该项目不错或者对你有所帮助,欢迎到 github 上给个 star,谢谢。项目地址:前台展示: https://github.com/乐趣区/blog-react管理后台:https://github.com/乐趣区/blog-react-admin后端:https://github.com/乐趣区/blog-nodeblog:https://github.com/乐趣区/blog本博客系统的系列文章:react + node + express + ant + mongodb 的简洁兼时尚的博客网站react + Ant Design + 支持 markdown 的 blog-react 项目文档说明基于 node + express + mongodb 的 blog-node 项目文档说明服务器小白的我,是如何将node+mongodb项目部署在服务器上并进行性能优化的9. 最后小汪也是第一次搭建 node 后端项目,也参考了其他项目。参考项目:1. nodepress2. React-Express-Blog-Demo对 全栈开发 有兴趣的朋友,可以扫下方二维码,关注我的公众号,我会不定期更新有价值的内容。微信公众号:乐趣区分享 前端、后端开发 等相关的技术文章,热点资源,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享–Python、Java、Linux、Go、node、vue、react、javaScript ...

November 25, 2018 · 16 min · jiezi

react + node + express + ant + mongodb 的简洁兼时尚的博客网站

前言此项目是用于构建博客网站的,由三部分组成,包含前台展示、管理后台和后端。此项目是基于 react + node + express + ant + mongodb 的,项目已经开源,项目地址在 github 上,喜欢的,欢迎给个 star 。项目地址:前台展示: https://github.com/乐趣区/blog-react管理后台:https://github.com/乐趣区/blog-react-admin后端:https://github.com/乐趣区/blog-node1. 效果图1.1 前台展示前台展示目前只支持 pc 端。1.2 管理后台管理后台是在蚂蚁金服用户开源的 ANT DESIGN PRO 基础上进行开发的。2. 体验地址网站主页: http://乐趣区.cn/main.html 网站首页:http://乐趣区.cn/管理后台:https://preview.pro.ant.design/user/login3. 计划这次是一个完整的全栈式开发,只要部署了这三个项目的代码,是完全可以搭建好博客网站的。作为一个后端的小白,在这次开发中,小汪也遇到了很多问题。往后的时间里,我会就这三个项目,推出相应的三篇文章教程或者说明和踩到的坑,敬请期待。4. 收获与感触学而不用,基本等于没学,所以为了有 react 相关的技术栈的实战经验,所以用了 react ,而且后端技术 node.js 和 mongodb 也是这一个多月里现学现用的,所以项目中肯定还有很多我不知道的实用技巧,如果写的不好的地方,请大家指出。网站前端部分如果用 vue 相关技术栈来完成的话,会更好更快,因为本人专长的是 vue 相关的技术栈。因为最近一直在做自己的个人博客网站,所以好久没更新技术文章了;而且是利用业余时间做的,所以经过差不多两个月的搬砖,现在网站终于都上线了。开发网站的这段时间里,每天晚上几乎都搬砖到接近 11 点,周末的时间大多也在搬砖,今晚写完这篇文章,也快 12 点了,搬砖不易啊,喜欢或者觉得不错的,欢迎到 github 上给个 star,谢谢。最后对 全栈开发 有兴趣的朋友可以扫下方二维码关注我的公众号,我会不定期更新有价值的内容。微信公众号:乐趣区分享 前端、后端开发等相关的技术文章,热点资源,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享–Python、Java、Linux、Go、node、vue、react、javaScript

November 22, 2018 · 1 min · jiezi

koa,express,node 通用方法连接MySQL

这个教程不管node,express,koa都可以用下面方法连接,这里用koa做个参考新建文件目录,我是这样子的很多教程都没有涉及到版本,所以让很多初学者,拷贝他的代码,出现错误问题我的版本: “dependencies”: { “koa”: “^2.6.2”, “mysql”: “^2.16.0” }1.设置配置文件// default.js// 设置配置文件const config = { // 启动端口 port: 3000, // 数据库配置 database: { DATABASE: ‘ceshi’, USERNAME: ‘root’, PASSWORD: ‘1234’, PORT: ‘3306’, HOST: ’localhost’ } } module.exports = config2.连接数据库// mysql/index.jsvar mysql = require(‘mysql’);var config = require(’../config/default.js’)var pool = mysql.createPool({ host : config.database.HOST, user : config.database.USERNAME, password : config.database.PASSWORD, database : config.database.DATABASE});class Mysql { constructor () { } query () { return new Promise((resolve, reject) => { pool.query(‘SELECT * from ceshidata’, function (error, results, fields) { if (error) { throw error }; resolve(results) // console.log(‘The solution is: ‘, results[0].solution); }); }) }}module.exports = new Mysql()3.设置服务器// index.jsconst Koa = require(‘koa’)const config = require(’./config/default’)const mysql = require(’./mysql’)const app = new Koa()app.use(async (ctx) => { let data = await mysql.query() ctx.body = { “code”: 1, “data”: data, “mesg”: ‘ok’ } })app.listen(config.port)console.log(listening on port ${config.port})4.启动服务器,去浏览器访问先去数据库添加点数据node index.js打开浏览器localhost:3000, 然后你就会看到以下数据,自己添加的数据查询出来了然后其他相关操作,可以看mysql相关API,我下次也会分享出来首发于微信公众号:node前端不妨关注一下,我们一起学习回复:100有福利哦 ...

November 15, 2018 · 1 min · jiezi

Node+Express+MySql实现简单增删改查和登录

var express = require(’express’);var mysql = require(‘mysql’);var app = express();var bodyParser = require(‘body-parser’);//链接数据库var connection = mysql.createConnection({ host: ’localhost’, user: ‘root’, password: ‘123456’, database: ‘school’});connection.connect();// 创建 application/x-www-form-urlencoded 编码解析(post方法)var urlencodedParser = bodyParser.urlencoded({ extended: false })//设置跨域访问app.all(’’, function (req, res, next) { res.header(“Access-Control-Allow-Origin”, “”); res.header(“Access-Control-Allow-Headers”, “X-Requested-With”); res.header(“Access-Control-Allow-Methods”, “PUT,POST,GET,DELETE,OPTIONS”); res.header(“Content-Type”, “application/json;charset=utf-8”); next();});//登录app.post(’/login’,urlencodedParser, function (req, res) { var username = req.body.username; var password = req.body.password; var sql = select * from login where username = '${username}' and password = '${password}'; connection.query(sql, function (err, result) { console.log(result) if (err || result.length == 0) { res.status(200), res.json(“登陆失败”) } else { res.status(200), res.json(“登陆成功”) } });})//查询app.get(’/query’, function (req, res) { var sql = ‘select * from student’; connection.query(sql, function (err, result) { if (err) { console.log(’err:’, err.message); } console.log(result); res.status(200), res.json(result) });});//修改app.get(’/change’, function (req, res) { var SNO = req.query.SNO; var SNAME = req.query.SNAME; var SDEPT = req.query.SDEPT; var sql = update student set SNAME = '${SNAME}',SDEPT = '${SDEPT}' where SNO = '${SNO}'; connection.query(sql, function (err, result) { if (err) { console.log(’err:’, err.message); } console.log(result); res.status(200), res.json(“修改成功”) });})//添加app.get(’/add’, function (req, res) { console.log(req.query) var SNO = req.query.SNO; var SNAME = req.query.SNAME; var SDEPT = req.query.SDEPT; var sql = insert into student values ('${SNO}','${SNAME}','${SDEPT}'); connection.query(sql, function (err, result) { if (err) { console.log(’err:’, err.message); } console.log(result); res.status(200), res.json(“添加成功”) });})//删除app.get(’/delete’, function (req, res) { console.log(req.query) var SNO = req.query.SNO; var sql = delete from student where SNO='${SNO}'; connection.query(sql, function (err, result) { if (err) { console.log(’err:’, err.message); } console.log(result); res.status(200), res.json(“删除成功”) });})// connection.end();//配置服务端口var server = app.listen(8080, function () { var host = server.address().address; var port = server.address().port; console.log(‘http://’, host, port);})github:https://github.com/Rossy11/no… ...

October 26, 2018 · 2 min · jiezi

express中使用es6

express官网上给的javascript标准为es5,是不能直接跑es6的,想要在express中使用es6写法,可以用转码器Babel进行转码。开发环境中express项目中安装babel-cli$ npm install –save-dev babel-cli安装presetsnpm install –save-dev babel-preset-es2015 babel-preset-stage-2在package.json里添加运行的脚本{ … “scripts”: { “start”: “babel-node index.js –presets es2015,stage-2” } …}到此就可以使用es6的写法了,写一段es6运行npm start刚开始学习express的时候,会遇到一个问题:每次改一点点代码,都需要重启服务。我们希望能够实现“热更新”的效果,接下来我们就可以使用nodemon监视文件修改,达到热更新效果,而不比每次都重启服务安装nodemonnpm install –save-dev nodemon修改脚本{ … “scripts”: { “start”: “nodemon index.js –exec babel-node –presets es2015,stage-2” } …}运行 npm start现在更改js代码,不需要重启服务,就可以实现效果了

October 17, 2018 · 1 min · jiezi

async语法升级踩坑小记

从今年过完年回来,三月份开始,就一直在做重构相关的事情。 就在今天刚刚上线了最新一次的重构代码,希望高峰期安好,接近半年的Node.js代码重构。 包含从callback+async.waterfall到generator+co,统统升级为了async,还顺带推动了TypeScript在我司的使用。 这些日子也踩了不少坑,也总结了一些小小的优化方案,进行精简后将一些比较关键的点,拿出来分享给大家,希望有同样在做重构的小伙伴们可以绕过这些。为什么要升级首先还是要谈谈改代码的理由,毕竟重构肯定是要有合理的理由的。 如果单纯想看升级相关事项可以直接选择跳过这部分。Callback从最原始的开始说起,期间确实遇到了几个年代久远的项目,Node 0.x,使用的普通callback,也有一些会应用上async.waterfall这样在当年看起来很优秀的工具。// 普通的回调函数调用var fs = require(‘fs’)fs.readFile(’test1.txt’, function (err, data1) { if (err) return console.error(err) fs.readFile(’test2.txt’, function (err, data2) { if (err) return console.error(err) // 执行后续逻辑 console.log(data1.toString() + data2.toString()) // … })})// 使用了async以后的复杂逻辑var async = require(‘fs’)async.waterfall([ function (callback) { fs.readFile(’test1.txt’, function (err, data) { if (err) callback(err) callback(null, data.toString()) }) }, function (result, callback) { fs.readFile(’test2.txt’, function (err, data) { if (err) callback(err) callback(null, result + data.toString()) }) }], function (err, result) { if (err) return console.error(err) // 获取到正确的结果 console.log(result) // 输出两个文件拼接后的内容})虽说async.waterfall解决了callback hell的问题,不会出现一个函数前边有二三十个空格的缩进。 但是这样的流程控制在某些情况下会让代码变得很诡异,例如我很难在某个函数中选择下一个应该执行的函数,而是只能按照顺序执行,如果想要进行跳过,可能就要在中途的函数中进行额外处理:async.waterfall([ function (callback) { if (XXX) { callback(null, null, null, true) } else { callback(null, data1, data2) } }, function (data1, data2, isPass, callback) { if (isPass) { callback(null, null, null, isPass) } else { callback(null, data1 + data2) } }])所以很可能你的代码会变成这样,里边存在大量的不可读的函数调用,那满屏充斥的null占位符。 所以callback这种形式的,一定要进行修改, 这属于难以维护的代码。Generator实际上generator是依托于co以及类似的工具来实现的将其转换为Promise,从编辑器中看,这样的代码可读性已经没有什么问题了,但是问题在于他始终是需要额外引入co来帮忙实现的,generator本身并不具备帮你执行异步代码的功能。 不要再说什么async/await是generator的语法糖了 因为我司Node版本已经统一升级到了8.11.x,所以async/await语法已经可用。 这就像如果document.querySelectorAll、fetch已经可以满足需求了,为什么还要引入jQuery呢。 所以,将generator函数改造为async/await函数也是势在必行。期间遇到的坑将callback的升级为async/await其实并没有什么坑,反倒是在generator + co 那里遇到了一些问题:数组执行的问题在co的代码中,大家应该都见到过这样的:const results = yield list.map(function * (item) { return yield getData(item)})在循环中发起一些异步请求,有些人会告诉你,从yield改为async/await仅仅替换关键字就好了。 那么恭喜你得到的results实际上是一个由Promise实例组成的数组。const results = await list.map(async item => { return await getData(item)})console.log(results) // [Promise, Promise, Promise, …]因为async并不会判断你后边的是不是一个数组(这个是在co中有额外的处理)而仅仅检查表达式是否为一个Promise实例。 所以正确的做法是,添加一层Promise.all,或者说等新的语法await*,Node.js 10.x貌似还不支持。。// 关于这段代码的优化方案在下边的建议中有提到const results = await Promise.all(list.map(async item => { return await getData(item)}))console.log(results) // [1, 2, 3, …]await / yield 执行顺序的差异这个一般来说遇到的概率不大,但是如果真的遇到了而栽了进去就欲哭无泪了。 首先这样的代码在执行上是没有什么区别的:yield 123 // 123await 123 // 123这样的代码也是没有什么区别的:yield Promise.resolve(123) // 123await Promise.resolve(123) // 123但是这样的代码,问题就来了:yield true ? Promise.resolve(123) : Promise.resolve(233) // 123await true ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>从字面上我们其实是想要得到yield那样的效果,结果却得到了一个Promise实例。 这个是因为yield、await两个关键字执行顺序不同所导致的。 在MDN的文档中可以找到对应的说明:MDN | Operator precedence 可以看到yield的权重非常低,仅高于return,所以从字面上看,这个执行的结果很符合我们想要的。 而await关键字的权重要高很多,甚至高于最普通的四则运算,所以必然也是高于三元运算符的。 也就是说await版本的实际执行是这样子的:(await true) ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>那么我们想要获取预期的结果,就需要添加()来告知解释器我们想要的执行顺序了:await (true ? Promise.resolve(123) : Promise.resolve(233)) // 123一定不要漏写 await 关键字这个其实算不上升级时的坑,在使用co时也会遇到,但是这是一个很严重,而且很容易出现的问题。 如果有一个异步的操作用来返回一个布尔值,告诉我们他是否为管理员,我们可能会写这样的代码:async function isAdmin (id) { if (id === 123) return true return false}if (await isAdmin(1)) { // 管理员的操作} else { // 普通用户的操作}因为这种写法接近同步代码,所以遗漏关键字是很有可能出现的:if (isAdmin(1)) { // 管理员的操作} else { // 普通用户的操作}因为async函数的调用会返回一个Promise实例,得益于我强大的弱类型脚本语言,Promise实例是一个Object,那么就不为空,也就是说会转换为true,那么所有调用的情况都会进入if块。 那么解决这样的问题,有一个比较稳妥的方式,强制判断类型,而不是简单的使用if else,使用类似(a === 1)、(a === true)这样的操作。_eslint、ts 之类的都很难解决这个问题_一些建议何时应该用 async ,何时应该直接用 Promise首先,async函数的执行返回值就是一个Promise,所以可以简单地理解为async是一个基于Promise的包装:function fetchData () { return Promise().resolve(123)}// ==>async function fetchData () { return 123}所以可以认为说await后边是一个Promise的实例。 而针对一些非Promise实例则没有什么影响,直接返回数据。 在针对一些老旧的callback函数,当前版本的Node已经提供了官方的转换工具util.promisify,用来将符合Error-first callback规则的异步操作转换为Promise实例: 而一些没有遵守这样规则的,或者我们要自定义一些行为的,那么我们会尝试手动实现这样的封装。 在这种情况下一般会采用直接使用Promise,因为这样我们可以很方便的控制何时应该reject,何时应该resolve。 但是如果遇到了在回调执行的过程中需要发起其他异步请求,难道就因为这个Promise导致我们在内部也要使用.then来处理么?function getList () { return new Promise((resolve, reject) => { oldMethod((err, data) => { fetch(data.url).then(res => res.json()).then(data => { resolve(data) }) }) })}await getList()但上边的代码也太丑了,所以关于上述问题,肯定是有更清晰的写法的,不要限制自己的思维。 async也是一个普通函数,完全可以放在任何函数执行的地方。 所以关于上述的逻辑可以进行这样的修改:function getList () { return new Promise((resolve, reject) => { oldMethod(async (err, data) => { const res = await fetch(data.url) const data = await res.json() resolve(data) }) })}await getList()这完全是一个可行的方案,对于oldMethod来说,我按照约定调用了传入的回调函数,而对于async匿名函数来说,也正确的执行了自己的逻辑,并在其内部触发了外层的resolve,实现了完整的流程。 代码变得清晰很多,逻辑没有任何修改。合理的减少 await 关键字await只能在async函数中使用,await后边可以跟一个Promise实例,这个是大家都知道的。 但是同样的,有些await其实并没有存在的必要。 首先有一个我面试时候经常会问的题目:Promise.resolve(Promise.resolve(123)).then(console.log) // ?最终输出的结果是什么。 这就要说到resolve的执行方式了,如果传入的是一个Promise实例,亦或者是一个thenable对象(简单的理解为支持.then((resolve, reject) => {})调用的对象),那么resolve实际返回的结果是内部执行的结果。 也就是说上述示例代码直接输出123,哪怕再多嵌套几层都是一样的结果。 通过上边所说的,不知大家是否理解了 合理的减少 await 关键字 这句话的意思。 结合着前边提到的在async函数中返回数据是一个类似Promise.resolve/Promise.reject的过程。 而await就是类似监听then的动作。所以像类似这样的代码完全可以避免:const imgList = []async function getImage (url) { const res = await fetch(url) return await res.blob()}await Promise.all(imgList.map(async url => await getImage(url)))// ==>async function getImage (url) { const res = fetch(url) return res.blob()}await Promise.all(imgList.map(url => getImage(url)))上下两种方案效果完全相同。Express 与 koa 的升级首先,Express是通过调用response.send来完成请求返回数据的。 所以直接使用async关键字替换原有的普通回调函数即可。 而Koa也并不是说你必须要升级到2.x才能够使用async函数。 在Koa1.x中推荐的是generator函数,也就意味着其内部是调用了co来帮忙做转换的。 而看过co源码的小伙伴一定知道,里边同时存在对于Promise的处理。 也就是说传入一个async函数完全是没有问题的。 但是1.x的请求上下文使用的是this,而2.x则是使用的第一个参数context。 所以在升级中这里可能是唯一需要注意的地方,在1.x不要使用箭头函数来注册中间件。// expressexpress.get(’/’, async (req, res) => { res.send({ code: 200 })})// koa1.xrouter.get(’/’, async function (next) { this.body = { code: 200 }})// koa2.xrouter.get(’/’, async (ctx, next) => { ctx.body = { code: 200 }})小结重构项目是一件很有意思的事儿,但是对于一些注释文档都很缺失的项目来说,重构则是一件痛苦的事情,因为你需要从代码中获取逻辑,而作为动态脚本语言的JavaScript,其在大型项目中的可维护性并不是很高。 所以如果条件允许,还是建议选择TypeScript之类的工具来帮助更好的进行开发。 ...

September 28, 2018 · 3 min · jiezi