这是一个用来练手的小项目,使用webpack + vue + express 实现了简单的图片读取、上传预览功能。
挖个坑记录下这几天开发的过程,业务代码其实都还好, express、node都只使用了简单的路由、文件流的功能, webpack配置是最大的痛点。
写在前面
本项目启动了两个http服务:
3000端口用户本地开发,配合webpack完成实时打包热替换等功能。
3001端口提供api、静态资源路由。
目录结构
webpack
webpack配置部分参考了 vue-cli https://github.com/vuejs/vue-cli
webpack的基础属性就不再赘述了, 这里直接讲一些关键部分。
webpack-dev-middlware 、 webpack-hot-middleware
webpack-dev-middlware + webpack-hot-middleware 结合为我们打造了自动打包、热替换的本地服务器, 解放了我们的F5。 为了隔离不同的环境,我们可以写两套webpack配置, 根据不同的环境调用不同的配置文件 详见webpack.dev.conf.js、webpack.prod.conf.js。
但是需要特别注意:webpack-dev-middleware组件打包后的文件放是在内存, 所以output path不会有新文件, 开发环境下应该读取根目录。
配置如下:
修改入口entry, 这里entry需要添加 dev-client.js文件来实现热替换
entry: ['./build/dev-client.js', './project/src/index.js']
配置 publicPath
// 开发环境
output: {
publicPath: '/'
}
// 生产环境
output: {
publicPath: '//localhost:3001/static'
}
开发环境下publicPath 我使用了根目录。而生产环境这里我用了 ‘//localhost:3001/static’, 大家这里可以填写实际生产环境的静态资源地址。
dev-client.js
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo-true&reload=true')
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload()
}
})
webpack-dev-middleware 的publicPath需要与webpack配置里的publicPath保持一致。
引用插件HotModuleReplacementPlugin
plugins: [
new webpack.HotModuleReplacementPlugin()
]
最后我们要新建 dev-server.js文件 启动http服务
dev-server.js
var webpack = require('webpack')
var express = require('express')
var webpackDevMiddleware = require('webpack-dev-middleware')
var webpackHotMiddleware = require('webpack-hot-middleware')
var app = express()
var webpackConfig = require('./webpack.dev.conf')
var compiler = webpack(webpackConfig)
var devMiddleWare = webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath,
noInfo: true,
hot: true,
stats: {
colors: true
}
})
// 每当webpack打包时触发 compilation
var hotMiddleWare = webpackHotMiddleware(compiler)
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleWare.publish({ action: 'reload' })
cb()
})
})
app.use(devMiddleWare)
app.use(hotMiddleWare)
app.listen(3000, function() {
console.log('listening on 3000')
})
webpack 流程参考文章 http://taobaofed.org/blog/201...
webpack Node API 参考文章 https://doc.webpack-china.org...
html-webpack-plugin-after-emit 是webpack的一个事件,
写到这里我们的项目已经可以在本地跑起来啦, 我们可以创建两个npm命令, 方便我们开发和打包代码
// package.json
scripts: {
"dev": "cross-env NODE_ENV=dev node ./build/dev-server.js",
"build": "webpack --config ./build/webpack.prod.conf.js",
}
自动打开页面
我们可能希望在开启服务的时候自动打开页面
app.listen(3000, function() {
let uri = `http://localhost:3000`
console.log('start listening on 3000')
opn(uri)
})
opn是一个可以打开浏览器的插件。
安装
npm install opn --save-dev
webpack-html-plugin
使用webpack-html-plugin插件自动生成html, 注意开发环境下生成的文件在根目录
配置 webpack
plugins: [
new HtmlWebpackPlugin({
template: 'project/src/index.jade',
filename: 'index.html'
})
]
压缩文件
我们生产环境下需要压缩文件, 使用webpack自带组件 UglifyJsPlugin
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
如果使用了ES6语法,webpack 打包时可能会提示报错, 需要配置babel。
配置babel
安装babel-loader, 在webpack配置里给js添加babel-loader
npm install babel-loader --save-dev
{
test: /\.js$/,
loader: 'babel-loader'
}
不过仅仅这样是不能正常工作的, 必须配置babel插件。
在根目录里新建.babelrc文件, 预设对应测插件。以前我们可能安装需要babel-presets-2015、babel-pressets-2016、babel-preset-latest 等多个插件, 将ES6.ES7转为ES5。 这样有一个问题, 在支持大部分ES新特性的现代浏览器比如chrome上, 也做了全部的转换。 babel新提供了一个非常强大的插件 babel-presets-env,它可以根据不同的平台和目标浏览器智能的转换代码, 仅仅转换一些不支持的特性。
安装 babel-presets-env
npm install babel-presets-env --save-dev
.babelrc
{
"presets": [
["env", { "modules": false }]
]
}
配置babel之后, 浏览器可能会报错:'npm run dev Cannot read property 'EventSource' of undefined'。解决办法:在babel-loader里配置include
{
test: /\.js$/,
loader: 'babel-loader',
include:[path.resolve(__dirname,"./../project")]
}
proxyTable
我们的本地开发服务器跟后端服务器是跨域的(端口号不一致), 通过 http-proxy-middleware 插件, 配置proxyTable可以解决跨域问题。
配置映射表 dev-proxy.js
module.exports = {
'/getList': {
target: 'http://localhost:3001/',
changeOrigin: true
},
'/uploadImg': {
target: 'http://localhost:3001',
changeOrigin: true
},
'/delImg': {
target: 'http://localhost:3001',
changeOrigin: true
}
}
dev-server.js
var proxyMiddleware = require('http-proxy-middleware')
var config = require('./../config')
var proxyTable = config.dev.proxyTable
Object.keys(proxyTable).forEach(function(context) {
var options = proxyTable[context]
app.use(context, proxyMiddleware(options))
})
这样在我们请求的时候传入相对路径的api, http-proxy-middleware 就会自动帮我们把转发到 http://localhost:3001 啦, 跨域 不存在的~
最终配置文件
到这里webpack配置已经全部完成, 虽然还很粗糙Orz,但是已经满足我们的基本需求了,
webpack.dev.conf.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
var hotMiddlewareScript = 'webpack-hot-middleware/client?reload=true'
console.log()
var devConfig = {
entry: ['./build/dev-client.js', './project/src/index.js'],
devtool: 'source-map',
output: {
path: path.resolve(__dirname, './../static/'),
publicPath: '/',
filename: 'bundle.js'
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.common.js'
}
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: 'project/src/index.jade',
filename: 'index.html'
})
],
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include:[path.resolve(__dirname,"./../project")]
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.styl$/,
use: ['style-loader', 'css-loader','stylus-loader']
},
{
test: /\.jade$/,
use: 'jade-loader'
},
{
test: /\.stylus$/,
loader: 'stylus-loader'
},
{
test: /\.vue$/,
loader: ['vue-loader']
}
]
}
}
module.exports = devConfig
webpack.prod.conf.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
var config = {
entry: path.resolve(__dirname, './../project/src/index.js'),
output: {
path: path.resolve(__dirname, './../static/'),
publicPath: '//localhost:3001/static',
filename: 'bundle.js'
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.common.js'
}
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './../project/src/index.jade'),
filename: 'index.html'
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
],
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.styl$/,
use: ['style-loader', 'css-loader','stylus-loader']
},
{
test: /\.jade$/,
use: 'jade-loader'
},
{
test: /\.stylus$/,
loader: 'stylus-loader'
},
{
test: /\.vue$/,
loader: ['vue-loader']
}
]
}
}
module.exports = config
dev-server.js
var fs = require('fs')
var path = require('path')
var opn = require('opn')
var express = require('express')
var webpack = require('webpack')
var webpackDevMiddleware = require('webpack-dev-middleware')
var webpackHotMiddleware = require('webpack-hot-middleware')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')
var config = require('./../config')
var compiler = webpack(webpackConfig)
var app = express()
var proxyTable = config.dev.proxyTable
console.log('当前环境 => ' + process.env.NODE_ENV)
var devMiddleWare = webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath,
noInfo: true,
hot: true,
stats: {
colors: true
}
})
var hotMiddleWare = webpackHotMiddleware(compiler)
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleWare.publish({ action: 'reload' })
cb()
})
})
// set proxy
Object.keys(proxyTable).forEach(function(context) {
var options = proxyTable[context]
app.use(context, proxyMiddleware(options))
})
app.use(devMiddleWare)
app.use(hotMiddleWare)
app.use(express.static('static'))
devMiddleWare.waitUntilValid(() => {
_resolve()
})
app.listen(3000, function() {
let uri = `http://localhost:3000`
console.log('start listening on 3000')
opn(uri)
})
var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})
module.exports = {
redy: readyPromise,
close: () => {
server.close()
}
}
服务端代码
服务端监听3001端口号, 主要提供了3个api 读取图片列表, 上传图片,删除图片。 设置了首页路由,静态资源路由。
(待续)
node
实现基本的相册功能用到了 fs 读写文件流, 创建目录,删除文件等。
上传图片
上传图片这里采取的是form提交, 必须设置 enctype="multipart/form-data", 服务端才能获取正确的格式。
var bodyParse = require('body-parser')
app.use(bodyParse.json())
app.post('/uploadImg', upload.single('file'), function(req, res) {
let item = req.file
let extName = item.originalname.match(/(\.[^\.]+)$/)[1]
let targetPath = path.resolve(__dirname, './../static/uploads/' + new Date().getTime()) + extName
let is = fs.createReadStream(item.path)
let os = fs.createWriteStream(targetPath)
is.pipe(os)
is.on('end', function() {
res.json({
url: targetPath
})
})
})
(待续)
使用 pm2 管理进程
之前用过 supervisor 来监重启听代码。 但是最新发现pm2功能更强大,不仅可以重启代码, 可以在后台运行(再也不用担心不小心退出进程了),操作界面也更友好!
"scripts": {
"server": "supervisor ./server/main.js",
}
安装 pm2
npm install pm2 --save-dev
配置文件 process.yml
apps:
- script : server/main.js
watch : ['./build/dev-server.js']
- script : build/dev-server.js
watch : ['./server/main.js']
env :
NODE_ENV: development
env_production:
NODE_ENV: production
npm 命令
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "cross-env NODE_ENV=dev node ./build/dev-server.js",
"server": "supervisor ./server/main.js",
"build": "webpack --config ./build/webpack.prod.conf.js",
"preview": "node ./build/preview.js",
"start": "pm2 start process.yml"
}
npm install
安装依赖
npm run dev
启动本地开发服务器
npm run server
启动后端服务器
npm run build
构建生产环境前端代码
npm run preview
预览页面
npm start
启动本地开发服务器 & 后端服务器
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。