前言
本文的主要内容是,使用koa
搭建一个简单的web服务器,并通过webpack
配置react
、ES6
开发环境。
搭建一个项目,首先肯定是让代码运行起来,能在浏览器端访问编写的html。其次即引入js、css等一些静态文件。为了方便开发,可能还需引入一些框架、处理器,如React、sass、使用ES6等,那么要让浏览器识别,需要对这些进行转化。本文将对这些环节进行说明。
搭建node环境
node是什么
Node.js is a JavaScript
runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
简单说,Node.js
即运行在服务端的javascript,它是基于chrome的V8引擎。
目前有许多Node.js
的框架,如Express.js
、koa.js
等。本文将使用koa.js
来搭建项目环境。
Koa is a new web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. Through leveraging generators Koa allows you to ditch callbacks and greatly increase error-handling. Koa does not bundle any middleware within core, and provides an elegant suite of methods that make writing servers fast and enjoyable.
再简单介绍下koa,与Express相比,它非常小,通过组合不同的中间件generator
减少回调函数嵌套,同时也极大地改进了错误处理方式,使编程更加优雅。
koa教程可参考http://koajs.com/,本文不做过多介绍。
搭建koa环境
代码目录结构如下:
app.js——入口文件
client——存放html、css、js文件
views——存放html文件
public——存放静态资源
server/router.js——配置不同的路由
config——配置不同环境的变量
具体下文详细说明~
创建HTTP服务器
首先当然是先安装koa啦~
前提条件:已安装node
$ npm init //创建模块,输入模块名
$ npm install koa@1.2.4 -S //安装的是koa1
然后新建一个app.js
文件,引入koa
,并启动端口服务
/**********app.js***********/
var koa = require('koa');
var app = koa(); //1.初始化koa
//2.注入中间件(必须是generator function)
//this指向当前用户的请求
app.use(function* () {
this.body = 'Hello Koa'; //输出到页面的内容
});
var port = 3030;
app.listen(port); //3.为3030端口绑定koa应用,其创建并返回了一个HTTP服务器
console.log(`项目已启动,正在监听${port}`);
执行这个文件:
node --harmony app.js //使用harmony
然后我们就可以通过localhost:3030
去访问了。
可以在package.json
中配置script
:
/**********package.json***********/
"start": "node --harmony app.js"
这样就能通过npm run start
去启动项目了。
路由配置
上一小节,我们通过this.body
向页面输入内容。我们可以加上路由判断:
var path = require('path');
app.use(function *(){
if (this.path === '/') {
this.body = 'Hello Koa';
}
if (this.path === '/detail') {
this.body = 'Hello Detail';
}
});
显然,这个过于繁琐,无法在项目中使用。因此我们需要引入路由中间件koa-router
,这样就能控制不同路由下返回的页面。
$ npm install koa-router@5.4.1 -S //加版本号是因为不同版本用法不同
使用server/router.js
文件进行配置:
var router = require('koa-router')();
router.get('/', function* () {
this.body = 'Hello World';
});
router.get('/test', function* () {
this.body = 'test';
});
module.exports = router.routes();
在 app.js
文件中加上:
var router = require('./server/router');
app.use(router);
访问localhost:3030/test
即返回如下:
当然了,我们还是通过this.body
向页面输入内容。那么,如何返回相应的html文件呢?下一小节见分晓~
通过路由返回相应的html
基于MVC模式,我们要将html模板抽离到view中,方便维护。这就需要用到模板引擎了。koa提供了许多模板引擎,如ejs、xtemplate等,本文引入了 koa-swig
,因为最终将通过react实现组件化,因此未进行模板引擎对比。
首先引入:
$ npm install koa-swig -S
在app.js
文件中加上:
app.context.render = render({
root: path.join(__dirname, './view'), //连成需要的路径:/koa-test/view/
cache: false,
ext: 'html'
});
其中,path
、__dirname
均是node
模块提供的。path
是用于处理文件、目录路径的库,path.join()
返回结果是把所有参数用\
连成路径。__dirname
指的是当前模块的目录名(即app.js的目录名/koa-test
)。
然后再改写server/router.js
文件:
router.get('/', function* () {
yield this.render('./index'); //渲染view/index.html
});
如此,我们访问localhost:3030
就能访问我们保存在view文件夹下的index.html文件了。
引入静态文件
如上所述,我们已经能访问到相应的html文件了,那下一步便是引入css、js了。那有人会说,这有什么难的,直接在html文件引入不就可以了吗?那我们来看看传统的引入方法是否生效~~~
首先在相应的public/css
文件夹中添加的index.css
,加入如下代码:
/*********** public/css/index.css ***********/
body {
color: red;
}
然后在view/index.html
引入,如下:
/*********** view/index.html ***********/
<link rel="stylesheet" href="../public/css/index.css"/>
重新启动应用,发现页面如下,样式并没有生效
是不是觉得很奇怪?我们看看Network请求:
可以看出,此文件Not found了。
仔细看一看请求的URL,localhost:3000
进入了我们的服务器,没问题。但是 /public/css/index.css
这个路径是个啥?要知道我们的服务器并没有对这个 path 做处理,当然就找不到这个资源了。
最原始的方式,当然是识别这个 path,并返回相应的资源
const fs = require('fs');
app.use(async ctx => {
if (/\/css\/*/g.test(ctx.request.path)) {
ctx.response.type = 'text/css';
ctx.response.body = fs.createReadStream(`./client${ctx.request.path}`);
}
});
如果每加载一个静态资源都对路径做一次识别,那简直太可怕了...所以,就要用到静态资源路径配置的中间件了。koa-static-server
库是koa提供的 用于配置静态文件的路径。首先引入:
$ npm install koa-static-server -S
在app.js
文件中加上:
/*********** app.js ***********/
var serve = require('koa-static-server');
app.use(serve({
rootDir: __dirname + '/public', //服务的文件(即配置的静态文件路径
rootPath: '/public' //被重写的路径(即将用什么路径去访问此文件
}));
再次启动服务,可以发现我们的页面已经加载入css文件中写的样式啦~
经过这些配置,我们已经成功搭建了node环境,可以开始开发了~
使用webpack搭建react+ES6环境
目前项目中经常会使用react、ES6开发。由于这些框架、语言浏览器还不支持,我们需要将其转成浏览器支持的ES5。我们希望的是:开发时,搭建好的项目环境就能帮我们转换。那么就需要以下的工作啦~~
创建React页面
首先,安装React相关依赖:
$ npm install react react-dom -S
在client
文件中新建index.js
文件,用于编写我们的React组件,内容如下:
/*********** client/index.js ***********/
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
class Test extends Component {
constructor() {
super();
}
render() {
return (
<div className="content">Hello, React</div>
)
}
}
ReactDOM.render(<Test />,
document.getElementById('container')
);
使用webpack进行打包
webpack is a module bundler for modern JavaScript applications. When webpack processes your application, it recursively builds a dependency graph that includes every module your application needs, then packages all of those modules into a small number of bundles - often only one - to be loaded by the browser.
webpack 是一个前端资源加载和打包的工具,可以对CSS、JS、图片、字体等模块进行编译、打包处理。要编译不同类型的文件,首先需要安装相应的loader加载器,如下:
$ npm install webpack -D
$ npm install babel-loader babel-core babel-preset-react babel-preset-es2015 -D
webpack使用webpack.config.js
作为配置文件,是一个标准的commonJS规范的模块,如下:
var path = require('path');
module.exports = {
entry: './client/index.js',
output: {
path: path.join(__dirname, '/public/'),
filename: 'js/app.js'
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
loaders: [{
loader: "babel-loader",
test: /\.jsx?$/,
query: {
presets: ['es2015', 'react']
}
}]
}
};
webpack配置文件的各个参数
entry
entry
是入口文件,即需要编译的js
文件;可以是字符串、数组或对象。其相对路径是相对于当前文件的。
如上例,我们需要编译的即client/index.js
文件
ouput
output
指的是定义输出文件的格式,即编译后的文件。多个entry
文件只对应一个output
文件。其中有以下常用的参数:
path
:输出文件的绝对路径。按设定的目录结构,需编译到public/js
文件夹中;filename
:编译后的文件名,也可以为'[name].js'
,即以编译前文件名命名。
The publicPath specifies the public URL address of the output files when referenced in a browser.
publicPath
:网站引用文件时访问的路径,当在不同域上托管一些文件时可以使用这个参数。之后我们会用到。
resolve
resolve
:用于修改常用模块配置的属性。有许多配置项,如下:
-
extensions
: array类型,用于配置文件后缀名,它可以用于自动补全后缀。 -
alias
:为模块配置别名 -
root
:定义查找模块的路径,必须为绝对路径
举个栗子:
resolve: {
//从这里开始查找module
root: '/node_modules/',
//自动扩展文件后缀, 即require时可以不写后缀, 例如`Hello.jsx`就可以使用`import Hello from 'Hello'`;
extensions: ['.js', '.jsx'],
//为模块配置别名, 方便直接引用
alias: {
'test': '/utils/test.js' //引用此文件, 只需require('test')
}
}
module
module
提供了不同加载器loaders
处理不同的模块(JS、Sass、CSS、image等)。以下是关于loaders
的定义。
Loaders are transformations that are applied on a resource file of your app. They are functions (running in node.js) that take the source of a resource file as the parameter and return the new source.
简单说,即,使用加载器将浏览器不支持的语言转化为它支持的语言。
loaders
是array类型,是loader的集合,其中每一项loader有以下参数:
-
loader
:string类型,表示用来加载这种资源的loader,‘-loader'可以省略。可以使用!来连接不同的loader,将从右向左进行加载。 -
test
:表示匹配的资源类型,可以使用正则表达式。 -
exclude
:用于屏蔽不需要被loader处理的文件路径 -
include
:array类型,表示需要被loader处理的文件路径
举个栗子:
module: {
//加载器配置
loaders: [{
loader: "babel-loader", //转换react及ES6语法
test: /\.jsx?$/,
query: {
presets: ['es2015', 'react']
}
}, {
test: /\.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, //字体转换
loader: 'file-loader'
}]
}
注意,其中使用的所有的loader都需用npm
加载进来。
plugins
可以引用一些插件来满足不同的需求,例如ExtractTextPlugin
用于提取文件等,后文会再提到~
plugins: [
//引用的插件
]
以上只列举一些常用且在此文中用到的参数,webpack还有很多参数,详情见:http://webpack.github.io/docs...
开始编译React文件
根据我们上上节编写的webpack.config.js
文件,就可以进行打包了。在命令行运行:
webpack
执行后发现public/js
下多了一个app.js
文件,那就是我们编译后的文件。
在views/index.html
中加入:
/*********** view/index.html ***********/
<script type="text/javascript" src="/public/js/app.js"></script>
访问localhost: 3030
即可以看到:
使用Sass处理样式
之前我们只使用普通css来处理样式,且通过<style>
标签引入。对于预编译 css 语言,则需使用其他的loader处理,下面以 sass
为例:
首先加载:
npm install style-loader css-loader sass-loader node-sass -D
在client
文件中加入index.scss
,如下:
/*********** client/index.scss ***********/
#container {
font-size: 20px;
.content {
color: blue;
}
}
在client/index.js
中引入:
/*********** client/index.js ***********/
require('./index.scss');
在webpack.config.js
中加入loader:
module: {
loaders: [{
loader: "babel-loader", //转换react及ES6语法
test: /\.jsx?$/,
query: {
presets: ['es2015', 'react']
}
}, {
loader: "style-loader!css-loader!sass-loader", //从右向左执行
test: /\.(scss|sass)$/
}]
}
其中,css-loader
,实现在js中通过require
的方式引入cssstyle-loader
是将样式插入到页面的style标签中。
重新编译,访问localhost:3030
,便可以看到效果了。检查元素,我们可以看到:
编译后的css将样式插入到页面的<style>
标签中了。
注意,webpack2不支持省略-loader
提取css文件
上一小节中,我们是通过将css内嵌在js文件中,并将样式插入<style>
标签的方式引入css,但如果css文件较庞大,内嵌在js中会减慢加载速度,因此应当把css生成到单独的文件中。
那么就需引入插件了~~
npm install extract-text-webpack-plugin -D
这个插件是用来分离css的。使用如下:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
module: {
//加载器配置
loaders: [{
loader: "babel-loader", //转换react及ES6语法
test: /\.jsx?$/,
query: {
presets: ['es2015', 'react']
}
}, {
test: /\.(scss|sass)$/, //可以在js中直接require相应的scss
loader: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader!sass-loader"
})
}]
},
plugins: [
//将内嵌在js文件中的css抽离
new ExtractTextPlugin({
filename: 'css/vendor.css',
disable: false,
allChunks: true
})
]
};
filename
指的是抽离后的css文件,该路径是相对output
中的path
的。
重新webpack,可以看到public/css
文件下生成编译后的vendor.css
文件
自动构建
完成以上的步骤,我们就可以开始开发了。那么问题来了,每次写完代码都需要重新 webpack构建,且重新刷新网页才能看到效果,实在是又繁琐又无聊。
一种更好的方式是启动一个静态资源服务器,监听代码变化并自动打包构建、刷新页面。webpack官方提供了一个第三方库:
npm install webpack-dev-server -D
在package.json
中配置:
/**********package.json***********/
"build": "webpack-dev-server --port 4040 --progress --config webpack.config.js"
这句命令的意思是,在localhost: 7070
建立一个web服务器;通过--port
指定端口号,默认为8080端口;--progress
是显示代码的打包进度。
自动创建index.html页面
之前我们是手动创建views/index.html
文件,引入相关的资源文件,然后渲染该页面。
假设我们项目准备上线了,而开发时引入的资源文件是在热更新服务上的,与上线要使用的资源文件地址是不一样的,那每次上线或切到本地开发,都要进行修改?
下面就来介绍的插件HtmlWebpackPlugin
,就是为了解决这种问题。
The HtmlWebpackPlugin simplifies creation of HTML files to serve your webpack bundles. This is especially useful for webpack bundles that include a hash in the filename which changes every compilation.
简单点说,就是用来生成html文件的,另外可以帮我们注入css、js等资源文件。
npm install html-webpack-plugin -D
它包含以下参数:
-
title
:用于html的title -
filename
:注入的html的文件名,默认为index.html。注意:此路径相对于output
中的path
-
template
:模板文件路径,可以根据指定的模板文件生成特定的html文件 -
inject
:true | 'head' | 'body' | false,表示是否注入资源到特定的模板及注入的位置。若设置为true或body,javascript资源将被放至body元素底部,'head'则放至head元素中。注意:注入的资源文件路径是相对于publicPath
,而非当前路径 -
cache
:是否缓存,默认为true,即只在内容变化时才生成新文件 -
favicon
:注入html中的favicon的文件名 -
chunks
:允许只注入某些模块的资源(当entry中定义了多个入口文件,可针对其中的几项注入) -
excludeChunks
:与chunks相对,即排除注入某些模块的资源
上述只列出一些常用且下文将使用的参数,如需了解全部,请参考:https://github.com/jantimon/h...
通过了解的参数,我们在webpack.config.js中加入这些代码:
/*********** webpack.config.js ***********/
output: {
path: path.join(__dirname, '/public/'),
filename: 'js/[name].js',
publicPath: '/public'
},
plugins: [
htmlWebpackPlugin({
filename: 'views/index.html', //非相对于当前路径, 而是相对于output的path
template: clientPath + 'views/index.html', //未指定loader时, 默认使用ejs-loader
inject: true,
chunks: ['app']
})
]
重新打包,可以看到/public
路径下生成了views/index.html
文件,且分别在头部和底部注入了css、js,如下:
<!--------- public/views/index.html ------------>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<link href="/public/css/vendor.css" rel="stylesheet"></head>
<body>
<div id="container"></div>
<script type="text/javascript" src="/public/js/app.js"></script></body>
</html>
同时,要记得将app.js
中返回html的路径进行修改,并重新起服务:
/*********** app.js ***********/
app.context.render = render({
root: path.join(__dirname, '/public/views'),
cache: false,
ext: 'html'
});
但是我们发现,注入的并不是我们热更新服务上的资源文件。因此我们得区分开发、生产环境,并注入不同路径的资源文件。有两种方法,一种是通过区分环境写入不同的publicPath
;另一种是通过HtmlWebpackPlugin
的自定义变量,开发环境由我们自己注入资源,生产环境才依赖自动注入。
方法一:
/*********** webpack.config.js ***********/
//通过写入不同的publicPath
const dev = process.env.BUILD_ENV === 'dev';
output: {
path: path.join(__dirname, '/public/'),
filename: 'js/[name].js',
publicPath: dev ? 'http://localhost:8080/public/' : '/public'
} //区分开发、生产环境
方法二:
/*********** webpack.config.js ***********/
const dev = process.env.BUILD_ENV === 'dev';
plugins: [
new HtmlWebpackPlugin({
dev: dev, //加入自定义变量
filename: 'views/index.html',
template: clientPath + 'views/index.html',
inject: !dev, //只在生产环境自动注入
chunks: ['app']
})
]
在client/views/index.html
手动注入开发环境所需的资源文件:
<!--------- client/views/index.html ------------>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<% if(htmlWebpackPlugin.options.dev) { %>
<link rel="stylesheet" href="http://localhost:8080/public/css/vendor.css" />
<% } %>
</head>
<body>
<div id="container"></div>
<% if(htmlWebpackPlugin.options.dev) {%>
<script type="text/javascript" src="http://localhost:8080/public/js/app.js"></script>
<% } %>
</body>
</html>
这两种方法,均依赖process.env.BUILD_ENV
变量,process.env
是node提供的用于返回用户运行环境的。这个变量是在执行webpack时加上的。
//生产环境
BUILD_ENV=production webpack --config webpack.config.js
//开发环境
BUILD_ENV=dev webpack --config webpack.config.js
可以发现,通过区分环境,在生成的public/views/index.html
文件中注入的资源文件路径是不同的,符合我们的需求。
当然,我们每次运行时都要敲这么一长串的代码,很容易犯错,那么可以通过在package.json
中的script
中加上以下代码:
"scripts": {
"start": "node --harmony app.js",
"production": "BUILD_ENV=production webpack --config webpack.config.js",
"dev": "BUILD_ENV=dev webpack --config webpack.config.js",
"build": "webpack-dev-server --port 8080 --progress --hot --config webpack.config.js"
}
即可通过npm run start
启动服务,npm run build
启动热更新服务。
到此,我们就完成了html文件注入。
代码压缩
可以看到,引入的资源文件没有进行压缩,如果资源文件较大的话,加载时间就会很长。webpack
提供了一个压缩的插件UglifyJsPlugin
,使用如下:
/*********** webpack.config.js ***********/
const webpack = require('webpack');
plugins: {
new webpack.optimize.UglifyJsPlugin({
comments: false, //是否在输出时保留注释
compress: {
warnings: false //是否打印warning信息
}
})
}
compress
:boolean|object
,为false则不压缩
这是webpack官网提供的较为快速、高效的压缩方法~
最终,webpack文件如下:
/*********** webpack.config.js ***********/
var path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const dev = process.env.BUILD_ENV === 'dev';
const clientPath = path.join(__dirname, '/client/');
module.exports = {
entry: {
app: ['./client/index.js']
},
output: {
path: path.join(__dirname, '/public/'),
filename: 'js/[name].js',
publicPath: dev ? 'http://localhost:8080/public/' : '/public'
},
resolve: {
//自动扩展文件后缀, 即require时可以不写后缀, 例如`Hello.jsx`就可以使用`import Hello from 'Hello'`;
extensions: ['.js', '.jsx']
},
module: {
//加载器配置
loaders: [{
loader: "babel-loader", //转换react及ES6语法
test: /\.jsx?$/,
query: {
presets: ['es2015', 'react']
}
}, {
test: /\.(scss|sass)$/, //可以在js中直接require相应的scss
loader: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader!sass-loader"
})
}]
},
plugins: [
//将内嵌在js文件中的css抽离
new ExtractTextPlugin({
filename: 'css/vendor.css',
disable: false,
allChunks: true
}),
new webpack.optimize.UglifyJsPlugin({
comments: false,
compress: {
warnings: true
}
}),
new HtmlWebpackPlugin({
filename: 'views/index.html', //非相对于当前路径, 而是相对于output的path
template: clientPath + 'views/index.html', //未指定loader时, 默认使用ejs-loader
inject: true,
chunks: ['app']
})
]
};
结语
至此,我们已经将项目环境搭建起来了,可以开始开发了。
若有遗漏、错误欢迎指出~(整篇文章跨越了较长时间...拖延症惹的祸?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。