项目背景

最近做了两个vue的项目,一个是商城供货商端,做的是vue单页;另一个是商城用户端,做的是vue多页。事实上,现在的前端框架如angular,react,vue等的出现都是服务于web应用的,一般的实践也都是做spa。但是考虑到商城页面较多,图片较多,如果spa,可能占用内存过高,另外观察京东,淘宝web版本,也都是多页实现。所以还是选多页吧。
在这里忍不住要赞下vue。语法清晰简单;我之前也用过angular1,相比之下,vue的轻量化的组件化开发,模板渲染,单文件编写,丰富的生命周期hook,完善的文档;让人写起代码来都很爽,可以看作是吸收了angular和react的优点。如果是小的web应用或者是小的团队开发,我肯定优先选择vue。

供货商端单页实现

先看下目录结构和最终的效果图:






项目链接:http://saas-1014-100001.m.izhuazhua.com/appProducer/producerIndex.jsp

webpack代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var webpack = require('webpack')
module.exports = {
entry: './src/main.js',
output: {
path: './static',
publicPath: '/static/',
filename: 'build.js'
},
module: {
noParse: /es6-promise\.js$/,
loaders: [
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.js$/,
exclude: /node_modules|vue\/dist|vue-router\/|vue-loader\/|vue-hot-reload-api\//,
loader: 'babel'
},
{
test: /\.(png|jpg)$/,
loader: 'url-loader?limit=8192'
}
]
},
babel: {
presets: ['es2015'],
plugins: ['transform-runtime']
}
}
if (process.env.NODE_ENV === 'production') {
module.exports.plugins = [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new webpack.optimize.OccurenceOrderPlugin()
]
} else {
module.exports.devtool = '#source-map'
}

entry指定入口文件是src/main.js。生成的build.js文件到static目录下。loader中看下使用到了vue-loader来处理.vue结尾的文件。A .vue file is a custom file format that uses HTML-like syntax to describe a Vue component. Each .vue file consists of three types of top-level language blocks: , and 。本次项目所有的component都是采用这种单文件.vue的形式编写的,这也是官方推荐的,简洁直观。
而vue-loader就是为解析vue文件的loader,它有如下几个功能:

  • ES2015 enabled by default;(自动支持es2015语法)
  • Allows using other Webpack loaders for each part of a Vue component, for example SASS for and as module dependencies and handle them with Webpack loaders;(将style和template作为模块)
  • Can simulate scoped CSS for each component;(能够支持组件内样式,其实就是使用了属性选择器。对于每个class生成一个唯一的attr值。.goods_item[_v-6a5a53f4] 然后以的方式添加样式)
  • Supports component hot-reloading during development.(开发阶段支持组件的热加载)

我们看下入口文件main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import Vue from 'vue'
import Router from 'vue-router'
import App from './containers/App.vue'
import OrdersView from './components/Orders.vue'
import OrderView from './components/Order.vue'
// install router
Vue.use(Router)
// routing
var router = new Router()
router.map({
'/orders': {
component: function(resolve){
require(['./components/Orders.vue'], resolve)
}
},
'/order/:id': {
component: function(resolve){
require(['./components/Order.vue'], resolve)
}
}
})
router.redirect({
'*': '/orders'
})
router.start(App, '#app')

这里在main.js中创建了一个路由器模块router,并且配置了路由信息。在App.vue组件中有router-view作为容器,匹配到的页面会渲染在其中。vue-router的详细介绍在这里

1
2
3
4
5
6
7
<template>
<div id="wrapper">
<router-view
class="view"
keep-alive></router-view>
</div>
</template>

路由组件都很相似,vue-router就和angular的ui-router很接近。而且vue-router还提供了滚动位置的保持,省去了很多事情。总的来说vue单页应用其实上手很简单,而且vue官方还提供了脚手架工具vue-cli。连webpack的配置都可以自动完成。

商城多页实现

先看下目录结构和最终的效果图:







项目链接:

  1. http://saas-1014-100001.m.izhuazhua.com/appMarket/dist/home.jsp
  2. http://saas-1014-100001.m.izhuazhua.com/appMarket/dist/orders.jsp

多页下比较麻烦,首先webpack entry需要设置多个。这里设置的是container下的所有js作为入口js;在getEntry中使用到了node的glob模块,这个模块可以通过编写一个正则来匹配筛选文件。glob匹配文件的操作一般是异步的,但这里采用sync匹配。sync的返回值是一个文件的列表。module.exports.out的设置也因为测试环境很正式环境有所不同:

  • development环境中采用的是HtmlWebpackPlugin生成多个页面。
  • production环境由于最终页面使用的是jsp而不是html,所以HtmlWebpackPlugin插件自然没有效果,采用自己编写plugin,在回调中通过stats.toJson().assetsByChunkName;可以获取到js chunk带上hash之后的filename,再通过node原生fs模块,对src/下的jsp文件处理,替换hash之后的js url。生成新的jsp文件到dist中。
    这就是大概的思路。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//webpack.config.js
var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin'); // 将样式提取到单独的css文件中,而不是打包到js文件或使用style标签插入在head标签中
var HtmlWebpackPlugin = require('html-webpack-plugin'); // 生成自动引用js文件的HTML
var glob = require('glob');
var fs = require('fs');
// 根据项目具体需求,具体可以看上面的项目目录,输出正确的js和html路径
function getEntry(globPath) {
var entries = {}, basename, tmp, pathname;
glob.sync(globPath).forEach(function (entry) {
basename = path.basename(entry, path.extname(entry));
tmp = entry.split('/').splice(-3);
pathname = tmp.splice(0, 1) + '/' + basename; // 正确输出js和html的路径
entries[pathname] = entry;
});
return entries;
}
var entries = getEntry('./src/containers/*.js'); // 获得入口js文件
var chunks = Object.keys(entries);
module.exports = {
entry: entries,
resolve: {
extensions: ['', '.js', '.vue']
},
module: {
loaders: [
//{
// test: /\.css$/,
// 使用提取css文件的插件,能帮我们提取webpack中引用的和vue组件中使用的样式
// loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
//},
{
test: /\.vue$/,
loader: 'vue' //vue-loader
},
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules|vue\/dist|vue-router\/|vue-loader\/|vue-hot-reload-api\//,
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url',
query: {
limit: 10000,
name: './imgs/[name].[ext]?[hash:7]'
}
}
]
},
// vue: { //这里是对vue-loader的配置,http://vue-loader.vuejs.org/en/configurations/extract-css.html
// loaders: {
// css: ExtractTextPlugin.extract('css'),
// // you can also include <style lang="less"> or other langauges
// less: ExtractTextPlugin.extract("css!less")
// }
// },
babel: {
presets: ['es2015'],
plugins: ['transform-runtime']
},
plugins: [
// 提取公共模块
new webpack.optimize.CommonsChunkPlugin({
name: 'vendors', // 公共模块的名称
chunks: chunks, // chunks是需要提取的模块
minChunks: chunks.length
})
// 配置提取出的样式文件
// new ExtractTextPlugin('css/style.css')
]
};
var prod = process.env.NODE_ENV === 'prod'; //run dev. prod为prodution
console.log('process.env.NODE_ENV-------------------:'+process.env.NODE_ENV);
if (prod) {
//chunkhash不支持热更新。但是和chunk本身有关; hash则每次编译都不一样,但是它支持热更新
module.exports.output = {
path: path.resolve(__dirname, 'dist'), // html,css,js,图片等资源文件的输出路径,将所有资源文件放在Public目录
filename: 'js/[name].[chunkhash:8].js' // 每个入口js文件的生成配置 js/[name].[hash].js
};
module.exports.plugins = module.exports.plugins.concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new webpack.optimize.OccurenceOrderPlugin(),
function() {
this.plugin('done', function(stats){ //可以获取hash之后的filename
// console.log(stats.toJson().assetsByChunkName);
var chunkhashname = stats.toJson().assetsByChunkName;
var text, key, hashname, filename;
for(key in chunkhashname){
if(key != 'vendors'){
text = fs.readFileSync(key+'.jsp').toString();
hashname = chunkhashname[key];
filename = hashname.match(/\S*\/([^.]*)/i)[1];
text = text.replace('js/vendors.js', chunkhashname['vendors']); //替换vendors
text = text.replace(hashname.split('.')[0]+'.js', hashname);
fs.writeFileSync('dist/'+filename+'.jsp', text, 'utf-8');
}
}
fs.writeFileSync('dist/common.jsp', fs.readFileSync('src/common.jsp').toString(), 'utf-8');
fs.writeFileSync('dist/checklogin.jsp', fs.readFileSync('src/checklogin.jsp').toString(), 'utf-8');
});
}
]);
} else {
module.exports.output = {
path: path.resolve(__dirname, 'dist'), // html,css,js,图片等资源文件的输出路径,将所有资源文件放在Public目录
filename: 'js/[name].js' // 每个入口js文件的生成配置 js/[name].[hash].js
};
module.exports.devtool = 'source-map';
var pages = getEntry('./src/*.html');
for (var pathname in pages) {
if(pathname.indexOf('common')==-1){
// 配置生成的html文件。chunks限制插入html的chunk,可以直接打印chunks数组查看
var conf = {
filename: prod? pathname + '.html' : pathname + '.html',
template: pages[pathname], // 模板路径
inject: 'body', // js插入位置
minify: {
removeComments: true,
collapseWhitespace: false
},
chunks: ['vendors', 'src/'+pathname.slice(2)],
hash: false
};
// 需要生成几个html文件,就配置几个HtmlWebpackPlugin对象
module.exports.plugins.push(new HtmlWebpackPlugin(conf));
}
}
}
// src/containers/home.js
import Vue from 'vue'
import Home from '../components/home.vue'
import LoginUtil from '../utils/login.js'
let ut = new LoginUtil();
ut.checkLogin().then((data) => {
new Vue({
el: 'body',
components: { Home }
})
})

整体就是这样,每个页面都有一个主component渲染页面,而这个主要的component又是由很多小的component组成。