2

这是一个用来练手的小项目,使用webpack + vue + express 实现了简单的图片读取、上传预览功能。

挖个坑记录下这几天开发的过程,业务代码其实都还好, express、node都只使用了简单的路由、文件流的功能, webpack配置是最大的痛点。

源码 https://github.com/littlewood...

写在前面

本项目启动了两个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 启动本地开发服务器 & 后端服务器


小木头
162 声望7 粉丝