webpack 大法好
Webpack 是大家熟知的前端开发利器,它可以搭建包含热更新的开发环境,也可以生成压缩后的生产环境代码,还拥有灵活的扩展性和丰富的生态环境。但它的缺点也非常明显,那就是配置项又多又复杂,随便拿出某一个配置项(例如 rules
, plugins
, devtool
等等)都够写上一篇文章来说明它的 N 种用法,对新手造成极大的困扰。Vue.js(以下简称 Vue)绝大部分情况使用 webpack 进行构建,间接地把这个问题丢给了 Vue 的新手们。不过不论是 Vue 还是 webpack,其实他们都知道配置问题的症结所在,因此他们也想了各自的办法来解决这个问题,我们先看看他们的努力。
Vue cli 2.x - 提供开箱即用的配置
在之前一长段时间中,我们要初始化一个 Vue 项目,一般是使用 vue-cli 提供的 vue init
命令(这也是 Vue cli 的 v2 版本,之后简称 Vue cli 2.x)。而且通常一些比较有规模的项目都会使用 vue init webpack my-project
来使用 webpack 模板,那么上面提到的配置问题就来了。
为了解决这个问题, Vue 的做法是提供开箱即用的配置,即通过 vue init
出来的项目,默认生成的巨多的配置文件,截图如下:
开箱即用是保证了,但一旦要修改,就相当于是进入了一个黑盒,开发者对于一堆文件,一堆 JSON 望洋兴叹。
webpack 4 - 极大地简化配置
webpack 4 推出也有一年左右了,它的核心改动之一是极大地简化配置。它添加了 mode
,把一些显而易见的配置做成内置的。因此例如 NoEmitOnErrorsPlugin()
, UglifyJSPlugin()
等等都不必写了;分包用的 CommonsChunkPlugin()
也浓缩成了一个配置项 optimization.splitChunks
,并且已有能适应绝大部分情况的默认值。
据说 webpack 4 构建出来的代码的体积还更小了,因此这次升级显然是必要的。
Vue cli 3.x - 升级 webpack,还搞出了插件
大约小半年前,Vue cli 推出了 v3 版本,也是一个颠覆性的升级。它把核心精简为 @vue/cli
,把 webpack 搞成了 @vue/cli-service
, 把其他东西抽象为“插件”。这些插件包括 babel, eslint, Vuex, Unit Testing 等等,还允许自定义编写和发布。我不在这里介绍 Vue cli 3.x 的用法和生态,但从结果看,现在通过 vue create
创建的的 Vue 项目清爽了不少。
所以现在的问题是什么?
如果我们单纯开发一个前端 Vue 项目,webpack-dev-server 能帮助我们启动一个 nodejs 服务器并支持热加载,非常好用。可如果我们要开发的是一个 nodejs + Vue 的全栈项目呢?两者是不可能启动在同一个端口的。那我们能做的只是让 nodejs 启动在端口 A,让 Vue (webpack-dev-server) 启动在端口 B。而如果 Vue 需要发送请求访问 nodejs 提供的 API 时,还会遇上跨域问题,虽然可以通过配置 proxy 解决,但依然非常繁琐。而实质上,这是一整个项目的前后端而已,我们应该使用一条命令,一个端口来启动它们。
抛开 Vue,此类需求 webpack 本身其实是支持的。因为它除了提供 webpack-dev-server 之外,还提供了 webpack-dev-middleware。它以 express middleware 的方式,同样集成了热加载的功能。因此如果我们的 nodejs 使用的是 express 作为服务框架的话,我们可以以 app.use
的方式引入这个中间件,就可以达成两者的融合了。
再说回 Vue cli 3。它通过 vue-cli-service
命令,把 webpack 和 webpack-dev-server 包裹起来,这样用户就看不到配置文件了,达成了简洁的目的。不过实质上,配置文件依然存在,只是移动到了 node_modules/@vue/cli-service/webpack.config.js
而已。当然为了个性化需求,它也支持用户通过配置对象 (configureWebpack
) 或者链式调用 (chainWebpack
) 两种间接的方式,但不再提供直接修改配置文件的方式了。
然而致命的是,即便它提供了足够的方式修改配置,但它不能把 webpack-dev-server 变成 webpack-dev-middleware。这表示使用 Vue cli 3 创建的 Vue 部分和 nodejs(express) 部分是不能融合的。
怎么解决?
说了这么多,其实这就是我最近实际碰到的问题以及分析问题的思路。鉴于 Vue cli 3 黑盒的特性,我们无法继续使用它了(可能以后有升级能解决这个问题,至少目前不行)。而使用 Vue cli 2 又因为它内置的是 webpack 3 且配置文件一大堆,也让人无所适从。这么看,唯一剩下的路就只能自行使用并配置 webpack 4了,这也是本文的内容所在。
技术栈
nodejs 部分
目前比较主流的构建 nodejs 部分的 Web 框架是 express,且不说它的语法有多优雅,使用有多广泛等等,最主要的原因是刚才提过的 webpack-dev-middleware 就是一个 express 的中间件,因此两者可以无缝衔接。
可惜的是,在我实际的项目中,我使用了 koa 作为了我的 nodejs 框架。其实要说它比 express 好在哪里我也说不上来,也不是本文的重点。可能出于尝鲜的目的,或者团队技术栈统一的目的,或者其他鬼使神差的巧合,反正我用了它,而且开始时还没意识到有这个融合的问题,直到后来发现 webpack-dev-middleware 和 koa 是不兼容的,我内心有过一丝后悔……当然这是后话了。
本文以 koa 为基准。如果您使用的是 express,其实大同小异,而且更加简单。
Vue 部分
Vue 没什么好多说的,就一个版本,不存在 express / koa / 其他的选择。只是这里我没有使用 SSR,而是普通的 SPA 项目(单页应用,前端渲染)。
目录结构
既然是两个项目合体,总有一个目录结构的安排问题。这里我不谈每个项目内部需要如何组织,那是 Vue / koa 本身的问题,也是个人喜好的问题。我想谈的是这两者之间的组织方式,不外乎以下 3 种:(实际上也是个人喜好问题,见仁见智,这里只是统一一下表述,避免后续的混淆)
以下截图中的前后端项目均为独立项目,即融合之前的,可以单独运行的那种,所以能看到两份 package.json 和 package-lock.json
后端项目为基础,前端项目为子目录
除了红框中的 vue 目录外,其他都是 nodejs 的代码。而且因为我只是做个示意,所以 nodejs 代码其实也仅仅包含两个 index.js,public 目录和两个 package.json。实际的 nodejs 项目应该会有更多的代码,例如 actions(把每个路由处理单独到一个目录),middlewares(过所有路由的中间件)等等。
这个安排的思路是认为前端是整个项目的一部分(页面展示部分),所以 Vue 单独放在一个目录里面。我采用的就是这种结构。
前端项目为基础,后端项目为子目录
这就和前面一种相反,红框中的是后端代码。这么安排的理由可能是因为我们是前端开发者,所以把前端代码位于基础位置,后端提供的 API 辅助 Vue 的代码运行。
中立,不偏向任何人
看了前面两种,自然能想到这第三种办法。不过我认为这种办法纯粹没事儿找事儿,因为根据 npm 的要求,package.json 是必须放在根目录的,所以实际上想把两者完全分离并公平对待是弊大于利的(例如各类调用路径都会多几层),适合强迫症患者。
改造 Vue 部分
Vue 部分的改造点主要是:
-
package.json 融合到根目录(nodejs) 的 package.json 里面去。这里主要包括依赖 (
dependency
和devDependency
)以及执行命令(scripts
)两部分。其余的如browserslist
,engine
等 babel 可能用到的字段,因为 nodejs 代码不需要 babel,所以可以直接复制过去,不存在融合。 -
编写
webpack.config.js
。(因为 Vue cli 3 是自动生成且隐藏的,这个就需要自己写)
下面详细来看。
融合 package.json
刚才有提到过,像 browserslist
, engine
这类 babel 等使用的字段,因为 nodejs 端是不需要的,所以简单的复制过去即可。需要动脑的是依赖和命令。
依赖方面,其实前后端共用的依赖也基本不存在,所以实际上也是一个简单的复制。需要注意的是类似 vue
, vue-router
, webpack
, webpack-cli
等等都是 devDependency
,而不是 dependency
。真正需要放到 dependency
的,其实只有 @babel/runtime
这一个(因为使用了 plugin-transform-runtime
)。
命令方面,本身 Vue 必备的是“启动开发环境”和“构建”两条命令(可选的还有测试,这个我这里先不讨论)。因为开发环境需要和 nodejs 融合,所以这条我们放到 nodejs 部分说。剩下的是构建命令,常规操作是通过设置 NODE_ENV
为 production
来让 webpack 走入线上构建的情况。另外值得注意的是,因为现在 package.json 和 webpack.config.js 不在同级目录了,所以需要额外指定目录,命令如下:(cross-env
是一个相当好用的跨平台设置环境变量的工具)
{
"scripts": {
"build": "cross-env NODE_ENV=production webpack --config vue/webpack.config.js"
}
}
复制代码
编写 webpack.config.js
本文的重点不是 webpack 的配置方式,因此这里比较简略,不详细讲述每个配置项的含义
webpack.config.js 本质上是一个返回 JSON 的配置文件,我们会用到其中的几个 key。如果要了解 webpack 全部的配置项,可以查看 webpack 的中文网站介绍。另外如果不想分段查看,你可以在这里找到完整的 webpack.config.js。
mode
webpack 4 新增配置项,常规可选值 'production'
和 'development'
。这里我们根据 process.env.NODE_ENV
来确定值。
let isProd = process.env.NODE_ENV === 'production'
module.exports = {
mode: isProd ? 'production' : 'development'
}
复制代码
entry
定义 webpack 的入口。我们需要把入口设置为创建 Vue 实例的那个 JS,例如 vue/src/main.js
。
{
entry: {
"app": [path.resolve(__dirname, './src/main.js')]
}
}
复制代码
output
定义 webpack 的输出配置。在开发状态下,webpack-dev-middleware(以下简称 wdm)并不会真的去生成这个 dist
目录,它是通过一个内存文件系统,把文件输出到内存。所以这个目录仅仅是一个标识而已。
{
output: {
filename: '[name].[hash:8].js',
path: isProd ? resolvePath('../vue-dist') : resolvePath('dist'),
publicPath: '/'
}
}
复制代码
resolve
主要定义两个东西:webpack 处理 import
时自动添加的后缀顺序和供快速访问的别名。
{
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolvePath('src'),
}
}
}
复制代码
module(重点)
module
在 webpack 中主要确定如何处理项目中不同类型的模块。我们这里采用最常用的配法,即告诉 webpack,什么样的后缀文件用什么样的 loader 来处理。
{
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js?$/,
loader: 'babel-loader',
exclude: file => (
/node_modules/.test(file) && !/\.vue\.js/.test(file)
)
},
{
test: /\.less$/,
use: [
isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
'css-loader',
'less-loader'
]
},
{
test: /\.css$/,
use: [
isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
'css-loader'
]
}
]
}
}
复制代码
上述配置了 4 种文件的处理方式,它们分别是:
-
/\.vue$/
处理 Vue 文件,使用 Vue 专门提供的
vue-loader
。这个处理器做的事情就是把 Vue 里面的和