集成 Express、Parcel 和 Vue
最近终于动手整理项目的框架,总体思路和去年基本一致,“无状态”和“按功能模块分目录而不是按前后端”仍然是核心的思想,不过具体的工具发生了变化。
- 数据存取这边用 GraphQL 取代 PouchDB ,在灵活性和可预测间做一个平衡;
- 前端用 Vue 替换 Riot ,主要还是出于生态方面的考虑,虽然复杂度提升了一点,但是可用的第三方库丰富很多;
- 最后用 Parcel 替换 Webpack 和 dev.js,Webpack4 据说简洁了不少,但还是有阴影,Vue 如果自己写 dev.js 也要比 Riot 的难一点,先用 Parcel 偷个懒把。
当然首要目标还是简化日常开发,于是花了两个小时把 Express、Parcel 和 Vue 集成到一起。做到每次启动后端进程的时候,Parcel 也会同时开始工作,不需要执行另外的命令,也不会占用额外的端口。Parcel 如果不单独占用端口,在项目多的时候,配置起来会清爽很多。
目录与文件
.
├── index.js
├── dist
└── src
├── admin
│ ├── index.js
│ ├── index.html
│ ├── admin.js
│ └── components
└── app
├── index.js
├── index.html
├── app.js
└── components
目录和去年的思路一致,admin 和 app 都是功能模块,且结构相似,每个都包含自己的服务端文件和客户端文件。下面的内容以 admin 为例。
/index.js (根目录下的)
const express = require('express');
const app = express();
const admin = require('./src/admin');
app.use('/admin', admin);
app.use(express.static('dist'));
app.get('/', (req, res) => {
res.end('Hello, World!');
});
app.listen('3000');
这里启动了一个 express 实例监听 3000 端口,重点在于第 4-5 行,把 /src/admin/index.js 做为 middleware 挂载到 app 上,原理我在这里讲过正确模块化,express 的 app.use
第 7 行把 dist 做为静态文件目录,因为 Parcel 编译的结果都会放到这个这个目录里来,没有这一行的话,后面访问 Parcel middleware 提供的地址时,会出现找不到 JavaScript 文件的情况。
/src/admin/index.js
const path = require('path');
const express = require('express');
const app = express();
const Bundler = require('parcel-bundler');
const entry = path.join(__dirname, 'index.html');
const bundler = new Bundler(entry);
app.use('/index', bundler.middleware());
... 其他 admin 模块的接口
module.exports = app;
引入 parcel-bundler ,以同目录下的 index.html 为入口进行打包,并以 middleware 的形式集成到 express 实例中,也就是将新的入口文件集成到 /admin/index 这个地址
/src/admin/index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="app">
{{ message }}
</div>
<div id="app-2">
<span v-bind:title="message">
鼠标悬停几秒钟查看此处动态绑定的提示信息!
</span>
</div>
<div id="app-3">
<span v-if="seen">Now you see me</span>
</div>
<div id="app-4">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</div>
<div id="app-5">
<p>{{ message }}</p>
<button v-on:click="reverseMessage">逆转消息</button>
</div>
<div id="app-6">
<p>{{ message }}</p>
<input v-model="message">
</div>
<div id="app-7">
<ol>
<todo-item
v-for="item in groceryList"
v-bind:todo="item"
v-bind:key="item.id">
</todo-item>
</ol>
</div>
</body>
<script src="./admin.js"></script>
</html>
基于 vue 模版的 html 文件,内容是 vue 官方文档中的几个例子,
倒数第二行的 script 引用了同目录下的 admin.js,Parcel 编译后,这个 html 文件大体上保持原来的样子,只是这一行变成了引用编译后生成的文件名
<script src="./admin.20f23d0e.js"></script>
其中引用的 admin.20f23d0e.js 就是 Parcel 打包后的 JavaScript 文件。具体文件名根据文件内容会有所不同。
/src/admin/admin.js
import Vue from 'vue/dist/vue.esm.js';
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});
var app2 = new Vue({
el: '#app-2',
data: {
message: '页面加载于 ' + new Date().toLocaleString()
}
});
var app3 = new Vue({
el: '#app-3',
data: {
seen: true
}
});
var app4 = new Vue({
el: '#app-4',
data: {
todos: [
{ text: '学习 JavaScript' },
{ text: '学习 Vue' },
{ text: '整个牛项目' },
]
}
});
var app5 = new Vue({
el: '#app-5',
data: {
message: 'Hello Vue.js'
},
methods: {
reverseMessage: function () {
this.message = this.message.split('').reverse().join('');
}
}
});
var app6 = new Vue({
el: '#app-6',
data: {
message: 'Hello Vue!'
}
});
Vue.component('todo-item', {
props: ['todo'],
template: '<li>{{ todo.text }}</li>'
});
var app7 = new Vue({
el: '#app-7',
data: {
groceryList: [
{id: 0, text: '蔬菜' },
{id: 1, text: '奶酪' },
{id: 2, text: '随便其它什么人吃的东西' },
]
}
});
这个文件的内容是 vue 官网文档例子的 JavaScript 部分,此文件会被打包成 admin.20f23d0e.js ,需要注意的是第一行引用的是 vue/dist/vue.esm.js 而不是单纯的 vue ,不然会提示 You are using the runtime-only build。Vue 提供了多个版本,这里需要使用包含编译器的版本,具体可以参考官方文档中对不同文件名的说明
最后打包完的内容被放在了根目录下的 dist 目录中,而不是和 admin.js 同目录。配合上文提到的 /index.js 中的第 7 行的配置,就可以得到正确的结果了。
使用 node index.js 启动服务,parcel 也会被通过 api 调用的形式拉起来,访问 http://127.0.0.1:3000/admin/index 即可看到打包后的文件的运行效果。