前两部分我们已经完成了博客页面的展示和后台页面的展示:
- React技术栈+Express+Mongodb实现个人博客 -- Part 1 博客页面展示
- React技术栈+Express+Mongodb实现个人博客 -- Part 2 后台管理页面
本篇文章主要介绍如何使用Express + Mongodb
创建Server端,并介绍Webpack
,下一篇文章将介绍使用webpack
打包博客工程。
Express
在开始之前,我想先通过一个Demo介绍Express
,对NodeJs比较熟悉的可以直接跳过这部分。
先使用Express创建一个简单的添加人员的服务,提供的接口包括:
- 获取所有的人员名单
- 添加人员
- 修改人员姓名
- 删除人员
1. 添加Node Packages文件
创建package.json
文件,添加需要的Node Packages
{
"name": "node-api",
"main": "server.js",
"dependencies": {
"express": "~4.15.4",
"mongoose": "~4.11.11",
"body-parser": "~1.18.0"
}
}
-
Express
是一个Node Framework,用于创建server服务 -
mongoose
是一个ORM模块,用于连接MongoDB
数据库 -
body-parser
用于读取Post
请求body
里面的内容
2. 安装这些Node Packages
npm install
3. 创建server.js文件
// server.js
// 基础设置
// =============================================================================
// 引用需要的package
var express = require('express'); // 引用 express
var app = express(); // 定义我们的app
var bodyParser = require('body-parser');
// 添加bodyParse配置
// 这个设置方便我们在处理Post请求时获取body里面的内容
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
var port = process.env.PORT || 8080; // 设置端口
// api中需要的Route
// =============================================================================
var router = express.Router();
// 测试服务是否正常运行
router.get('/', function(req, res) {
res.json({ message: 'nice,服务器来了!' });
});
// 这里将会创建更多的路由
// 注册路由 -------------------------------
// 我们所有的接口将以 /api 开头
app.use('/api', router);
// 启动服务
// =============================================================================
app.listen(port);
console.log('服务正常运行在端口: port ' + port);
4. 启动服务
node server.js
使用PostMan来测试接口返回的数据:
5. 数据库和Modal
安装MongoDB
数据库
brew update
brew install mongodb
brew services start mongodb #启动mongodb
可以下载Robo 3T用于数据库的可视化管理。
在server.js
中使用mogoose
链接数据库:
// 启动服务
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/member', function (err) {
if (err) {
console.log(err, "数据库连接失败");
return;
}
console.log('数据库连接成功');
app.listen(port, function (err) {
if (err) {
console.error('err:', err);
} else {
console.info(`===> api server is running at localhost:27017`)
}
});
});
创建Member
的Modal
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var MemberSchema = new Schema({
name: String
})
module.exports = mongoose.model('Member', MemberSchema);
6. Express中的Router和Route
我们创建一个Express Router
来管理所有的Route
,先来看一下我们要创建的所有Route
:
Route | HTTP Verb | Description |
---|---|---|
/api/members | GET | 获取所有人员 |
/api/members | POST | 添加人员 |
/api/memebers/:member_id | GET | 获取指定id的人员 |
/api/memebers/:member_id | PUT | 修改指定人员信息 |
/api/memebers/:member_id | DELETE | 删除指定人员 |
7. Route Middleware
Express
里有个中间件(middleware)的概念。所谓中间件,就是在收到请求后和发送响应之前这个阶段执行的一些函数。
要在一条路由的处理链上插入中间件,可以使用express对象的use方法。该方法原型如下:
app.use([path,] function [, function...])
当app.use
没有提供path
参数时,路径默认为“/”
。当你为某个路径安装了中间件,则当以该路径为基础的路径被访问时,都会应用该中间件。比如你为“/abcd”设置了中间件,那么“/abcd/xxx”
被访问时也会应用该中间件。
// middleware to use for all requests
router.use(function(req, res, next) {
// do logging
console.log('Something is happening.');
next(); // make sure we go to the next routes and don't stop here
});
我们在根路由这里添加一个中间件,每次有接口调用时,都会被调用,可用于日志记录。
8. 创建Routes
router.route('/members')
.post(function(req, res) {
var member = new Member();
member.name = req.body.name;
member.save(function(err) {
if (err)
res.sned(err);
res.json({ message: 'Member created!' });
});
})
.get(function(req, res) {
Member.find(function(err, members) {
if (err)
res.send(err);
res.json(members);
});
});
router.route('/members/:member_id')
.get(function(req, res) {
Member.findById(req.params.bear_id, function(err, member) {
if (err)
res.send(err);
res.json(member);
})
})
.put(function(req, res) {
Member.findById(req.params.bear_id, function(err, member) {
if (err)
res.send(err)
member.name = req.body.name;
member.save(function(err) {
if (err)
res.send(err);
res.json({ message: 'Member updated!'})
})
});
})
.delete(function(req, res) {
Member.remove({
_id: req.params.bear_id
}, function(err, member) {
if (err)
res.send(err)
res.json({ message: 'Successfully deleted! '});
})
});
可以使用PostMan
测试接口是否正确,这个Demo的完整代码在这里
通过上面的demo,你应该基本了解了
- 如何使用Express创建一个服务
- 如何使用mongoose,通过创建modal,实现数据的增删改查操作
- 如何控制路由
有了这些基础知识,开始编写博客的服务端吧。
博客服务端
因为react
中使用了react-router
,也就是说前端是一个单页面应用。所以我们后端几乎就可以不去考虑前端路由的配置了。也就是说浏览器的url地址请求的页面我们是不做任何处理的。 但是倘若不做忽略处理的话,当然是不实行的。因为当你刷新的时候,会报404.
server.js
import path from 'path'
import Express from 'express'
import connectHistoryApiFallback from 'connect-history-api-fallback'
const app = new Express();
const port = config.port;
app.use('/',Express.static(path.join(__dirname,"..",'build')));
app.use('/', connectHistoryApiFallback());
connect-history-api-fallback
即可完全处理完了前端的路由。然后Express.static
就是我们项目中需要托管的静态资源文件。
API代理
这样的话,后段部分不需要怎么关心前端页面的路由了,而可以集中精力去处理前端的API请求。所以这里,我想把前端页面服务器和处理API请求的服务器分开。也就是说,所有关系到 API请求的部分,我再另起一个服务器,在页面服务中做一个代理,转发到另一个API server 上。 代码如下:
import httpProxy from 'http-proxy'
import config from '../config/config'
const app = new Express();
const port = config.port;
app.use('/api',(req,res)=>{
proxy.web(req,res,{target:targetUrl})
});
const targetUrl = `http://${config.apiHost}:${config.apiPort}`;
const proxy = httpProxy.createProxyServer({
target:targetUrl
});
如上代码,就可以做到当我访问/api
的时候,HTTPProxy
会将我的请求代理到targetUrl
上,也就是api
服务器。在API server
上我们做一些关于数据库的操作以及对前端传过来数据的操作。
api server
import Express from 'express'
import config from '../../config/config'
import bodyParser from 'body-parser'
import mongoose from 'mongoose'
import cookieParser from 'cookie-parser'
import session from 'express-session'
const port = config.apiPort;
const app = new Express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser('express_react_cookie'));
app.use(session({
secret:'express_react_cookie',
resave: true,
saveUninitialized:true,
cookie: {maxAge: 60 * 1000 * 30}//过期时间
}));
//展示页面路由
app.use('/', require('./main'));
//管理页面路由
app.use('/admin', require('./admin'));
mongoose.Promise = require('bluebird');
mongoose.connect(`mongodb://${config.dbHost}:${config.dbPort}/blog`, function (err) {
if (err) {
console.log(err, "数据库连接失败");
return;
}
console.log('数据库连接成功');
app.listen(port, function (err) {
if (err) {
console.error('err:', err);
} else {
console.info(`===> api server is running at ${config.apiHost}:${config.apiPort}`)
}
});
});
如代码所示,API server 我们做了如下几件事
- 新起一个http server
- 使用express.router来路由API请求的路由
- 使用 mongoose 来连接 MongoDB 数据库
- 使用 bodyParse 中间件来解析post请求的请求体数据。
关于express-session和cookie-parser这个后面关于免登陆的博客会介绍到。
api server 路由
express
的路由非常的简单,其实和react
思想都是差不多的。就相当于对你请求路径的一层层筛选过滤。
比如我们上面的
//展示页面路由
app.use('/', require('./main'));
//管理页面路由
app.use('/admin', require('./admin'));
那么现在让我们看看admin
和 main
中到底写了些什么
// admin.js
import Express from 'express'
const router = Express.Router();
import User from '../../models/user'
import {responseClient} from '../util'
//admin请求后台验证
router.use( (req,res,next) =>{
if(req.session.userInfo){
next()
}else{
res.send(responseClient(res,200,1,'身份信息已过期,请重新登录'));
}
});
router.use('/tags',require('./tags'));
router.use('/article',require('./article'));
router.get('/getUsers',(req,res)=>{
let skip =(req.query.pageNum-1)<0?0:(req.query.pageNum-1)*10;
let responseData = {
total:0,
list:[]
};
User.count()
.then(count=>{
responseData.total = count;
User.find(null,'_id username type password',{skip:skip,limit:10})
.then((result)=>{
responseData.list = result;
responseClient(res,200,0,'',responseData)
})
.catch(err=>{
responseClient(res);
})
});
});
module.exports = router;
我们使用express.Router
来产生一个router
对象,别迷糊,我们完全可以理解为这个router
就是一个express
的实例。
注意这个router.get('/getUsers')
真正匹配的完整请求路径是:/api/admin/getUsers
User
是我们创建的Modal
用于数据库对象的各种操作。
responseCliend
使我们封装的一个对象,用于response
结果的封装:
responseClient(res,httpCode = 500, code = 3,message='服务端异常',data={}) {
let responseData = {};
responseData.code = code;
responseData.message = message;
responseData.data = data;
res.status(httpCode).json(responseData)
}
main.js
中主要包含3个路由:
- 获取全部标签
router.use('/user', require('./user'));
//获取全部标签
router.get('/getAllTags', function (req, res) {
Tags.find(null, 'name').then(data => {
responseClient(res, 200, 0, '请求成功', data);
}).catch(err => {
responseClient(res);
})
});
- 获取文章
router.get('/getArticles', function (req, res) {
let tag = req.query.tag || null;
let isPublish = req.query.isPublish;
let searchCondition = {
isPublish,
};
if (tag) {
searchCondition.tags = tag;
}
if (isPublish === 'false') {
searchCondition = null
}
let skip = (req.query.pageNum - 1) < 0 ? 0 : (req.query.pageNum - 1) * 5;
let responseData = {
total: 0,
list: []
};
Article.count(searchCondition)
.then(count => {
responseData.total = count;
Article.find(searchCondition, '_id title isPublish author viewCount commentCount time coverImg', {
skip: skip,
limit: 5
})
.then(result => {
responseData.list = result;
responseClient(res, 200, 0, 'success', responseData);
}).cancel(err => {
throw err
})
}).cancel(err => {
responseClient(res);
});
});
- 获取文章详情
router.get('/getArticleDetail', (req, res) => {
let _id = req.query.id;
Article.findOne({_id})
.then(data=>{
data.viewCount = data.viewCount+1;
Article.update({_id},{viewCount:data.viewCount})
.then(result=>{
responseClient(res,200,0,'success',data);
}).cancel(err=>{
throw err;
})
}).cancel(err => {
responseClient(res);
});
});
另外,我将标签管理,文章管理单独放在一个文件里,方便扩展和管理:
// article.js
import Express from 'express'
const router = Express.Router();
import Article from '../../models/article'
import {responseClient} from '../util'
router.post('/addArticle', function (req, res) {
const {
title,
content,
time,
tags,
isPublish
} = req.body;
const author = req.session.userInfo.username;
const coverImg = `/${Math.round(Math.random() * 9 + 1)}.jpg`;
const viewCount = 0;
const commentCount = 0;
let tempArticle = new Article({
title,
content,
isPublish,
viewCount,
commentCount,
time,
author,
coverImg,
tags:tags.split(',')
});
tempArticle.save().then(data=>{
responseClient(res,200,0,'保存成功',data)
}).cancel(err=>{
console.log(err);
responseClient(res);
});
});
router.post('/updateArticle',(req,res)=>{
const {
title,
content,
time,
tags,
isPublish,
id
} = req.body;
Article.update({_id:id},{title,content,time,tags:tags.split(','),isPublish})
.then(result=>{
console.log(result);
responseClient(res,200,0,'更新成功',result)
}).cancel(err=>{
console.log(err);
responseClient(res);
});
});
router.get('/delArticle',(req,res)=>{
let id = req.query.id;
Article.remove({_id:id})
.then(result=>{
if(result.result.n === 1){
responseClient(res,200,0,'删除成功!')
}else{
responseClient(res,200,1,'文章不存在');
}
}).cancel(err=>{
responseClient(res);
})
});
module.exports = router;
// tags.js
import Express from 'express'
const router = Express.Router();
import Tags from '../../models/tags'
import {responseClient} from '../util'
//删除标签
router.get('/delTag', function (req, res) {
let {name} = req.query;
Tags.remove({name})
.then(result => {
if(result.result.n === 1){
responseClient(res,200,0,'删除成功!')
}else{
responseClient(res,200,1,'标签不存在');
}
}).catch(err => {
responseClient(res);
});
});
//添加标签
router.post('/addTag', function (req, res) {
let {name} = req.body;
Tags.findOne({
name
}).then(result => {
if (!result) {
let tag = new Tags({
name
});
tag.save()
.then(data => {
responseClient(res, 200, 0, '添加成功', data);
}).catch(err => {
throw err
})
} else {
responseClient(res, 200, 1, '该标签已存在');
}
}).catch(err => {
responseClient(res);
});
});
module.exports = router;
以上就是我们所有的route
,满足博客端的所有场景。
关于后端的路由,也就是/api/xxx
请求后面的xxx
我们该如何设计,这个因人而异。自己怎么舒服怎么来。
然后该博客的后端服务起的两个服务:一个是前端页面服务,一个是API服务。所有的请求统一到页面 server
中。约束api请求的写法,当为/api
请求发来时,我们则通过HTTPProxy
来代理到API server
上。
数据库的连接、/api
请求后的后端路由控制,都是在api server
上完成的。
Webpack
接下来会用比较多的篇幅介绍Webpack
,初学者对Webpack
会有很多疑惑,比如loaders
和plugins
。如果你对Webpack
已经比较了解了,可以跳过该部分。
什么是WebPack,为什么要使用它?
现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript
代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法
- 模块化,让我们可以把复杂的程序细化为小的文件;
- 类似于
TypeScript
这种在JavaScript
基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为-
JavaScript
文件使浏览器可以识别; -
Scss
,less
等CSS
预处理器 - ...
这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack
类的工具的出现提供了需求。
什么是Webpack
WebPack
可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript
模块以及其它的一些浏览器不能直接运行的拓展语言(Scss
,TypeScript
等),并将其转换和打包为合适的格式供浏览器使用。
开始使用Webpack
初步了解了Webpack
工作方式后,我们一步步的开始学习使用Webpack
。
安装
Webpack
可以使用npm
安装,新建一个空的练习文件夹(此处命名为sample project
),在终端中转到该文件夹后执行下述指令就可以完成安装。
//全局安装
npm install -g webpack
//安装到你的项目目录
npm install --save-dev webpack
正式使用Webpack前的准备
- 在上述练习文件夹中创建一个
package.json
文件,这是一个标准的npm
说明文件,里面蕴含了丰富的信息,包括当前项目的依赖模块,自定义的脚本任务等等。在终端中使用npm init
命令可以自动创建这个package.json
文件
npm init
输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可。
-
package.json
文件已经就绪,我们在本项目中安装Webpack
作为依赖包
// 安装Webpack
npm install --save-dev webpack
- 回到之前的空文件夹,并在里面创建两个文件夹,
app
文件夹和public
文件夹,app
文件夹用来存放原始数据和我们将写的JavaScript
模块,public
文件夹用来存放之后供浏览器读取的文件(包括使用webpack
打包生成的js文件以及一个index.html
文件)。接下来我们再创建三个文件:
-
index.html
--放在public
文件夹中; -
Greeter.js
-- 放在app
文件夹中; -
main.js
-- 放在app
文件夹中;
此时项目结构如下图所示
我们在
index.html
文件中写入最基础的
html
代码,它在这里目的在于引入打包后的
js
文件(这里我们先把之后打包后的
js
文件命名为
bundle.js
,之后我们还会详细讲述)。
Webpack Sample Project
我们在Greeter.js
中定义一个返回包含问候信息的html
元素的函数,并依据CommonJS
规范导出这个函数为一个模块:
// Greeter.js
module.exports = function() {
var greet = document.createElement('div');
greet.textContent = "Hi there and greetings!";
return greet;
};
main.js
文件中我们写入下述代码,用以把Greeter
模块返回的节点插入页面。
//main.js
const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());
正式使用Webpack
webpack
可以在终端中使用,在基本的使用方法如下:
# {extry file}出填写入口文件的路径,本文中就是上述main.js的路径,
# {destination for bundled file}处填写打包文件的存放路径
# 填写路径的时候不用添加{}
webpack {entry file} {destination for bundled file}
指定入口文件后,webpack
将自动识别项目所依赖的其它文件,不过需要注意的是如果你的webpack
不是全局安装的,那么当你在终端中使用此命令时,需要额外指定其在node_modules
中的地址,继续上面的例子,在终端中输入如下命令
# webpack非全局安装的情况
node_modules/.bin/webpack app/main.js public/bundle.js
结果如下
可以看出
webpack
同时编译了
main.js
和
Greeter,js
,现在打开
index.html
,可以看到如下结果
有没有很激动,已经成功的使用Webpack
打包了一个文件了。不过在终端中进行复杂的操作,其实是不太方便且容易出错的,接下来看看Webpack
的另一种更常见的使用方法。
通过配置文件来使用Webpack
Webpack
拥有很多其它的比较高级的功能(比如说本文后面会介绍的loaders
和plugins
),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript
模块,我们可以把所有的与打包相关的信息放在里面。
继续上面的例子来说明如何写这个配置文件,在当前练习文件夹的根目录下新建一个名为webpack.config.js
的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。
module.exports = {
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/public",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
}
}
注:“__dirname”是node.js中的一个全局变量,它指向当前执行脚本所在的目录。
有了这个配置之后,再打包文件,只需在终端里运行webpack
(非全局安装需使用node_modules/.bin/webpack
)命令就可以了,这条命令会自动引用webpack.config.js
文件中的配置选项,示例如下:
又学会了一种使用Webpack
的方法,这种方法不用管那烦人的命令行参数,有没有感觉很爽。如果我们可以连webpack
(非全局安装需使用node_modules/.bin/webpack
)这条命令都可以不用,那种感觉会不会更爽~,继续看下文。
更快捷的执行打包任务
在命令行中输入命令需要代码类似于node_modules/.bin/webpack
这样的路径其实是比较烦人的,不过值得庆幸的是npm
可以引导任务执行,对npm
进行配置后可以在命令行中使用简单的npm start
命令来替代上面略微繁琐的命令。在package.json
中对scripts
对象进行相关设置即可,设置方法如下:
{
"name": "webpack-sample-project",
"version": "1.0.0",
"description": "Sample webpack project",
"scripts": {
"start": "webpack" // 修改的是这里,JSON文件不支持注释,引用时请清除
},
"author": "zhang",
"license": "ISC",
"devDependencies": {
"webpack": "^3.8.1"
}
}
注:
package.json
中的script
会安装一定顺序寻找命令对应位置,本地的node_modules/.bin
路径就在这个寻找清单中,所以无论是全局还是局部安装的Webpack
,你都不需要写前面那指明详细的路径了。
npm
的start
命令是一个特殊的脚本名称,其特殊性表现在,在命令行中使用npm start
就可以执行其对于的命令,如果对应的此脚本名称不是start
,想要在命令行中运行时,需要这样用npm run {script name}
如npm run build
,我们在命令行中输入npm start
试试,输出结果如下:
现在只需要使用npm start
就可以打包文件了,有没有觉得webpack
也不过如此嘛,不过不要太小瞧webpack
,要充分发挥其强大的功能我们需要修改配置文件的其它选项,一项项来看。
Webpack的强大功能
生成Source Maps(使调试更容易)
开发总是离不开调试,方便的调试能极大的提高开发效率,不过有时候通过打包后的文件,你是不容易找到出错了的地方,对应的你写的代码的位置的,Source Maps
就是来帮我们解决这个问题的。
通过简单的配置,webpack
就可以在打包时为我们生成的source maps
,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。
在webpack
的配置文件中配置source maps
,需要配置devtool
,它有以下四种不同的配置选项,各具优缺点,描述如下:
devtool | 配置结果 |
---|---|
source-map |
在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map ,但是它会减慢打包速度; |
cheap-module-source-map |
在一个单独的文件中生成一个不带列映射的map ,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便; |
eval-source-map |
使用eval 打包源文件模块,在同一个文件中生成干净的完整的source map 。这个选项可以在不影响构建速度的前提下生成完整的sourcemap ,但是对打包后输出的JS 文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项 |
cheap-module-eval-source-map |
这是在打包文件时最快的生成source map 的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map 选项具有相似的缺点; |
正如上表所述,上述选项由上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对打包后的文件的的执行有一定影响。
对小到中型的项目中,eval-source-map
是一个很好的选项,再次强调你只应该开发阶段使用它,我们继续对上文新建的webpack.config.js
,进行如下配置:
module.exports = {
devtool: 'eval-source-map',
entry: __dirname + "/app/main.js",
output: {
path: __dirname + "/public",
filename: "bundle.js"
}
}
cheap-module-eval-source-map
方法构建速度更快,但是不利于调试,推荐在大型项目考虑时间成本时使用。
使用webpack构建本地服务器
想不想让你的浏览器监听你的代码的修改,并自动刷新显示修改后的结果,其实Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js
构建,可以实现你想要的这些功能,不过它是一个单独的组件,在webpack
中进行配置之前需要单独安装它作为项目依赖
npm install --save-dev webpack-dev-server
devserver
作为webpack
配置选项中的一项,以下是它的一些配置选项,更多配置可参考这里
devserver的配置选项 | 功能描述 |
---|---|
contentBase |
默认webpack-dev-server 会为根文件夹提供本地服务器,如果想为另外一个目录下的文件提供本地服务器,应该在这里设置其所在目录(本例设置到“public"目录) |
port |
设置默认监听端口,如果省略,默认为”8080“ |
inline |
设置为true ,当源文件改变时会自动刷新页面 |
historyApiFallback |
在开发单页应用时非常有用,它依赖于HTML5 history API ,如果设置为true ,所有的跳转将指向index.html |
把这些命令加到webpack
的配置文件中,现在的配置文件webpack.config.js
如下所示
module.exports = {
devtool: 'eval-source-map',
entry: __dirname + "/app/main.js",
output: {
path: __dirname + "/public",
filename: "bundle.js"
},
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true//实时刷新
}
}
在package.json
中的scripts
对象中添加如下命令,用以开启本地服务器:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack",
"server": "webpack-dev-server --open"
},
在终端中输入npm run server
即可在本地的8080
端口查看结果
Loaders
Loaders
是webpack
提供的最激动人心的功能之一了。通过使用不同的loader
,webpack
有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss
为css
,或者把下一代的JS
文件(ES6
,ES7
)转换为现代浏览器兼容的JS
文件,对React
的开发而言,合适的Loaders
可以把React
的中用到的JSX
文件转换为JS
文件。
Loaders
需要单独安装并且需要在webpack.config.js
中的modules
关键字下进行配置,Loaders
的配置包括以下几方面:
-
test
:一个用以匹配loaders所处理文件的拓展名的正则表达式(必须) -
loader
:loader的名称(必须) -
include/exclude
:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选); -
query
:为loaders提供额外的设置选项(可选)
不过在配置loader
之前,我们把Greeter.js
里的问候消息放在一个单独的JSON
文件里,并通过合适的配置使Greeter.js
可以读取该JSON
文件的值,各文件修改后的代码如下:
{
"greetText": "Hi there and greetings from JSON!"
}
更新后的Greeter.js
var config = require('./config.json');
module.exports = function() {
var greet = document.createElement('div');
greet.textContent = config.greetText;
return greet;
};
注 由于
webpack3.*/webpack2.*
已经内置可处理JSON
文件,这里我们无需再添加webpack1.*
需要的json-loader
。在看如何具体使用loader
之前我们先看看Babel
是什么
Babel
Babel
其实是一个编译JavaScript
的平台,它的强大之处表现在可以通过编译帮你达到以下目的:
- 使用下一代的
JavaScript
代码(ES6
,ES7
...),即使这些标准目前并未被当前的浏览器完全的支持; - 使用基于
JavaScript
进行了拓展的语言,比如React
的JSX
;
Babel的安装与配置
Babel
其实是几个模块化的包,其核心功能位于称为babel-core
的npm
包中,webpack
可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析Es6
的babel-preset-es2015
包和解析JSX
的babel-preset-react
包)。
我们先来一次性安装这些依赖包
// npm一次性安装多个依赖模块,模块之间用空格隔开
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react
在webpack
中配置Babel
的方法如下:
module.exports = {
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/public",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
},
devtool: 'eval-source-map',
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true//实时刷新
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader",
options: {
presets: [
"es2015", "react"
]
}
},
exclude: /node_modules/
}
]
}
};
现在你的webpack的配置已经允许你使用ES6
以及JSX
的语法了。继续用上面的例子进行测试,不过这次我们会使用React
,记得先安装 React
和 React-DOM
npm install --save react react-dom
接下来我们使用ES6的语法,更新Greeter.js
并返回一个React
组件
//Greeter,js
import React, {Component} from 'react'
import config from './config.json';
class Greeter extends Component{
render() {
return (
{config.greetText}
);
}
}
export default Greeter;
修改main.js
如下,使用ES6的模块定义和渲染Greeter
模块
// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';
render( , document.getElementById('root'));
重新使用npm start
打包,如果之前打开的本地服务器没有关闭,你应该可以在localhost:8080
下看到与之前一样的内容,这说明react
和es6
被正常打包了。
Babel的配置
Babel
其实可以完全在 webpack.config.js
中进行配置,但是考虑到babel
具有非常多的配置选项,在单一的webpack.config.js
文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel
的配置选项放在一个单独的名为 .babelrc
的配置文件中。我们现在的babel
的配置并不算复杂,不过之后我们会再加一些东西,因此现在我们就提取出相关部分,分两个配置文件进行配置(webpack
会自动调用.babelrc
里的babel
配置选项),如下:
module.exports = {
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/public",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
},
devtool: 'eval-source-map',
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true//实时刷新
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
}
]
}
};
//.babelrc
{
"presets": ["react", "es2015"]
}
到目前为止,我们已经知道了,对于模块,Webpack
能提供非常强大的处理功能,那哪些是模块呢。
一切皆模块
Webpack
有一个不可不说的优点,它把所有的文件都都当做模块处理,JavaScript
代码,CSS
和fonts
以及图片等等通过合适的loader
都可以被处理。
CSS
webpack
提供两个工具处理样式表,css-loader
和 style-loader
,二者处理的任务不同,css-loader
使你能够使用类似@import
和 url(...)
的方法实现 require()
的功能,style-loader
将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack
打包后的JS
文件中。
继续上面的例子
//安装
npm install --save-dev style-loader css-loader
//使用
module.exports = {
...
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{
loader: "style-loader"
}, {
loader: "css-loader"
}
]
}
]
}
};
请注意这里对同一个文件引入多个loader的方法。
接下来,在app文件夹里创建一个名字为"main.css"的文件,对一些元素设置样式
/* main.css */
html {
box-sizing: border-box;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin: 0;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1, h2, h3, h4, h5, h6, p, ul {
margin: 0;
padding: 0;
}
我们这里例子中用到的webpack
只有单一的入口,其它的模块需要通过 import
, require
, url
等与入口文件建立其关联,为了让webpac
k能找到main.css
文件,我们把它导入main.js
中,如下
//main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';
import './main.css';//使用require导入css文件
render( , document.getElementById('root'));
通常情况下,
css
会和js
打包到同一个文件中,并不会打包为一个单独的css
文件,不过通过合适的配置webpack
也可以把css
打包为单独的文件的。
CSS module
在过去的一些年里,JavaScript
通过一些新的语言特性,更好的工具以及更好的实践方法(比如说模块化)发展得非常迅速。模块使得开发者把复杂的代码转化为小的,干净的,依赖声明明确的单元,配合优化工具,依赖管理和加载管理可以自动完成。
不过前端的另外一部分,CSS
发展就相对慢一些,大多的样式表却依旧巨大且充满了全局类名,维护和修改都非常困难。
最近有一个叫做CSS modules
的技术就意在把JS的模块化思想带入CSS
中来,通过CSS
模块,所有的类名,动画名默认都只作用于当前模块。Webpack
从一开始就对CSS
模块化提供了支持,在CSS loader
中进行配置后,你所需要做的一切就是把modules
传递到所需要的地方,然后就可以直接把CSS
的类名传递到组件的代码中,且这样做只对当前组件有效,不必担心在不同的模块中使用相同的类名造成冲突。具体的代码如下
module.exports = {
...
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
modules: true
}
}
]
}
]
}
};
在app
文件夹下创建一个Greeter.css
文件
.root {
background-color: #eee;
padding: 10px;
border: 3px solid #ccc;
}
导入.root
到Greeter.js
中
import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css';//导入
class Greeter extends Component{
render() {
return (
//添加类名
{config.greetText}
);
}
}
export default Greeter;
放心使用把,相同的类名也不会造成不同组件之间的污染。
CSS modules
也是一个很大的主题,有兴趣的话可以去官方文档查看更多消息
CSS预处理器
Sass
和 Less
之类的预处理器是对原生CSS
的拓展,它们允许你使用类似于variables, nesting, mixins, inheritance
等不存在于CSS
中的特性来写CSS
,CSS
预处理器可以这些特殊类型的语句转化为浏览器可识别的CSS
语句,
你现在可能都已经熟悉了,在webpack
里使用相关loaders进行配置就可以使用了,以下是常用的CSS
处理loaders
:
Less Loader
Sass Loader
Stylus Loader
不过其实也存在一个CSS
的处理平台-PostCSS
,它可以帮助你的CSS实现更多的功能,在其官方文档可了解更多相关知识。
举例来说如何使用PostCSS
,我们使用PostCSS
来为CSS
代码自动添加适应不同浏览器的CSS
前缀。
首先安装postcss-loader
和 autoprefixer
(自动添加前缀的插件)
npm install --save-dev postcss-loader autoprefixer
接下来,在webpack
配置文件中添加postcss-loader
,在根目录新建postcss.config.js
,并添加如下代码之后,重新使用npm start
打包时,你写的css
会自动根据Can i use
里的数据添加不同前缀了。
//webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
modules: true
}
}, {
loader: "postcss-loader"
}
]
}
]
}
}
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
至此,Webpack
部分的内容已经谈论了处理JS
的Babel
和处理CSS
的PostCSS
的基本用法,它们其实也是两个单独的平台,配合webpack
可以很好的发挥它们的作用。接下来介绍Webpack
中另一个非常重要的功能-Plugins
插件(Plugins)
插件(Plugins)是用来拓展Webpack
功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders
和Plugins
常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders
是在打包构建过程中用来处理源文件的(JSX,Scss,Less..)
,一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。
Webpack
有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。
使用插件的方法
要使用某个插件,我们需要通过npm
安装它,然后要做的就是在webpack配置中的plugins
关键字部分添加该插件的一个实例(plugins
是一个数组)继续上面的例子,我们添加了一个给打包后代码添加版权声明的插件。
const webpack = require('webpack');
module.exports = {
...
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
modules: true
}
}, {
loader: "postcss-loader"
}
]
}
]
},
plugins: [
new webpack.BannerPlugin('版权所有,翻版必究')
],
};
通过这个插件,打包后的JS文件显示如下
这就是webpack
插件的基础用法了,下面给大家推荐几个常用的插件
HtmlWebpackPlugin
这个插件的作用是依据一个简单的index.html
模板,生成一个自动引用你打包后的JS
文件的新index.html
。这在每次生成的js
文件名称不同时非常有用(比如添加了hash
值)。
安装
npm install --save-dev html-webpack-plugin
这个插件自动完成了我们之前手动做的一些事情,在正式使用之前需要对一直以来的项目结构做一些更改:
- 移除
public
文件夹,利用此插件,index.html
文件会自动生成,此外CSS
已经通过前面的操作打包到JS
中了。 - 在
app
目录下,创建一个index.tmpl.html
文件模板,这个模板包含title
等必须元素,在编译过程中,插件会依据此模板生成最终的html
页面,会自动添加所依赖的css, js,favicon
等文件,index.tmpl.html
中的模板源代码如下:
Webpack Sample Project
- 更新
webpack
的配置文件,方法同上,新建一个build
文件夹用来存放最终的输出文件
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/build",
filename: "bundle.js"
},
devtool: 'eval-source-map',
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true//实时刷新
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
modules: true
}
}, {
loader: "postcss-loader"
}
]
}
]
},
plugins: [
new webpack.BannerPlugin('版权所有,翻版必究'),
new HtmlWebpackPlugin({
template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
})
],
};
再次执行npm start
你会发现,build文件夹下面生成了bundle.js
和index.html
。
Hot Module Replacement
Hot Module Replacement(HMR)
也是webpack
里很有用的一个插件,它允许你在修改组件代码后,自动刷新实时预览修改后的效果。
在webpack
中实现HMR
也很简单,只需要做两项配置
- 在
webpack
配置文件中添加HMR
插件; - 在
Webpack Dev Server
中添加“hot”
参数;
不过配置完这些后,JS
模块其实还是不能自动热加载的,还需要在你的JS
模块中执行一个Webpack
提供的API
才能实现热加载,虽然这个API
不难使用,但是如果是React
模块,使用我们已经熟悉的Babel
可以更方便的实现功能热加载。
整理下我们的思路,具体实现方法如下
- Babel和webpack是独立的工具
- 二者可以一起工作
- 二者都可以通过插件拓展功能
- HMR是一个webpack插件,它让你能浏览器中实时观察模块修改后的效果,但是如果你想让它工作,需要对模块进行额外的配额;
- Babel有一个叫做react-transform-hrm的插件,可以在不对React模块进行额外的配置的前提下让HMR正常工作;
还是继续上例来实际看看如何配置
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/build",
filename: "bundle.js"
},
devtool: 'eval-source-map',
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true,
hot: true
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
modules: true
}
}, {
loader: "postcss-loader"
}
]
}
]
},
plugins: [
new webpack.BannerPlugin('版权所有,翻版必究'),
new HtmlWebpackPlugin({
template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
}),
new webpack.HotModuleReplacementPlugin()//热加载插件
],
};
安装react-transform-hmr
npm install --save-dev babel-plugin-react-transform react-transform-hmr
配置Babel
// .babelrc
{
"presets": ["react", "es2015"],
"env": {
"development": {
"plugins": [["react-transform", {
"transforms": [{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}]
}]]
}
}
}
现在当你使用React时,可以热加载模块了,每次保存就能在浏览器上看到更新内容。
产品阶段的构建
目前为止,我们已经使用webpack
构建了一个完整的开发环境。但是在产品阶段,可能还需要对打包的文件进行额外的处理,比如说优化,压缩,缓存以及分离CSS
和JS
。
对于复杂的项目来说,需要复杂的配置,这时候分解配置文件为多个小的文件可以使得事情井井有条,以上面的例子来说,我们创建一个webpack.production.config.js
的文件,在里面加上基本的配置,它和原始的webpack.config.js
很像,如下
// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
output: {
path: __dirname + "/build",
filename: "bundle.js"
},
devtool: 'eval-source-map',
devServer: {
contentBase: "./public", //本地服务器所加载的页面所在的目录
historyApiFallback: true, //不跳转
inline: true,
hot: true
},
module: {
rules: [{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
}, {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [{
loader: "css-loader",
options: {
modules: true
}
}, {
loader: "postcss-loader"
}],
})
}]
},
plugins: [
new webpack.BannerPlugin('版权所有,翻版必究'),
new HtmlWebpackPlugin({
template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
}),
new webpack.HotModuleReplacementPlugin() //热加载插件
],
};
//package.json
{
"name": "sample-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack",
"server": "webpack-dev-server --open",
"build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
},
"author": "",
"license": "ISC",
"devDependencies": {
"autoprefixer": "^7.1.6",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-react-transform": "^3.0.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.7",
"html-webpack-plugin": "^2.30.1",
"postcss-loader": "^2.0.8",
"react-transform-hmr": "^1.0.4",
"style-loader": "^0.19.0",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.3"
},
"dependencies": {
"async": "^2.5.0",
"react": "^16.0.0",
"react-dom": "^16.0.0"
}
}
优化插件
webpack
提供了一些在发布阶段非常有用的优化插件,它们大多来自于webpack
社区,可以通过npm
安装,通过以下插件可以完成产品发布阶段所需的功能
-
OccurenceOrderPlugin
:为组件分配ID,通过这个插件webpack可以分析和优先考虑使用最多的模块,并为它们分配最小的ID -
UglifyJsPlugin
:压缩JS代码; -
ExtractTextPlugin
:分离CSS和JS文件
我们继续用例子来看看如何添加它们,OccurenceOrder
和 UglifyJS plugins
都是内置插件,你需要做的只是安装其它非内置插件
npm install --save-dev extract-text-webpack-plugin
在配置文件的plugins后引用它们
// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/build",
filename: "bundle.js"
},
devtool: 'none',
devServer: {
contentBase: "./public",//本地服务器所加载的页面所在的目录
historyApiFallback: true,//不跳转
inline: true,
hot: true
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
modules: true
}
}, {
loader: "postcss-loader"
}
]
}
]
},
plugins: [
new webpack.BannerPlugin('版权所有,翻版必究'),
new HtmlWebpackPlugin({
template: __dirname + "/app/index.tmpl.html"
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin(),
new ExtractTextPlugin("style.css")
],
};
此时执行npm run build
可以看见代码是被压缩后的
缓存
缓存无处不在,使用缓存的最好方法是保证你的文件名和文件内容是匹配的(内容改变,名称相应改变)
webpack
可以把一个哈希值添加到打包的文件名中,使用方法如下,添加特殊的字符串混合体([name], [id] and [hash]
)到输出文件名前
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
..
output: {
path: __dirname + "/build",
filename: "bundle-[hash].js"
},
...
};
现在用户会有合理的缓存了。
以上是Webpack
的内容介绍,博客中使用到的知识都基本覆盖到了(demo的完整代码),下一节内容将介绍如何使用webpack
打包博客工程,又有哪些常用的plugins
总结
以上内容主要介绍了:
- Express的使用
- 博客中的server端api服务
- 介绍webpack使用
在介绍server端的服务中,部分代码使用了jsx
语法,所以到当前阶段博客应用还不能启动。下一节内容主要介绍使用Webpack
打包博客工程,并通过配置文件启动两个服务,博客端和server端。
系列文章
React技术栈+Express+Mongodb实现个人博客
React技术栈+Express+Mongodb实现个人博客 -- Part 1 博客页面展示
React技术栈+Express+Mongodb实现个人博客 -- Part 2 后台管理页面
React技术栈+Express+Mongodb实现个人博客 -- Part 3 Express + Mongodb创建Server端
React技术栈+Express+Mongodb实现个人博客 -- Part 4 使用Webpack打包博客工程
React技术栈+Express+Mongodb实现个人博客 -- Part 5 使用Redux
React技术栈+Express+Mongodb实现个人博客 -- Part 6 部署