Vue.js 使用 runtime only build

新项目中使用 Vue.js,随着开发的推进,对 Vue 的官方文档及源码的阅读和学习也在深入。与 React 的jsx相似, Vue 在底层的实现上,也是将模板编译成虚拟 DOM 渲染函数。而这个编译过程一般通过webpack 等构建工具提前完成,在代码运行时只需要引入runtime版本即可,无需实时编译,否则会对性能造成影响 。

使用 runtime 版本替代带编译器版本

Vue的官方文档中有对 Runtime + Compiler vs. Runtime-only的说明,如果项目中的Vue 实例没有用到 template 字符串或 dom 模板作为选项,则可以引入 runtime 版本的dist 文件,在 webpack 中可以配置一个别名予以简化引入。我们的项目使用了官方的webpack 工程模板(有经过改造),在这里用的还是编译器版本的,于是将其调整为 runtime版本:

1
2
3
4
5
6
7
8
9
module.exports = {
// ...
resolve: {
alias: {
// 'vue$': 'vue/dist/vue.esm.js',
'vue$': 'vue/dist/vue.runtime.esm.js',
}
}
}

使用 render 选项替代 template

项目中的组件模板都是通过单文件组件(*.vue文件)组织的,通过vue-loader已经提前编译成 render 函数,按理说可以使用 runtime 版本,但跑起来始终报 You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build. Debug 发现,原来入口文件main.js中初始化根组件时引入 App组件就用了 template 选项:'<App/>',问题就在这里,改动方法也比较简单,就是将其改为 render 选项:

1
2
3
4
5
6
new Vue({
el: '#app',
components: { App },
// template: '<App/>'
render: createElement => createElement(App)
})

render 选项传入的是一个回调函数,createElement 作为这个函数的参数,其实可以是任意一个有效函数名,但如文档所述,作为通用惯例,可以用h 来替代 createElement

总结

对于runtime版本的使用,官方文档都有详见的描述,但是由于使用了官方的 webpack 工程模板,默认使用了带编译器版本,而且没有对此细节的相关说明,也是踩了一个小坑。虽然官方说实时编译本来也已经很快了,但对大型应用来说,这个性能优化还是有必要的,而且runtime版本比带编译器版本的dist文件小了不少(生产构建版本当然不会差太多),也是能节省一点流量开销。

不过,要使用 runtime 版本得确保组件实例里不存在 字符串template 选项或dom模板,可以根据自己的实际项目需求来定。

webpack-dev-server proxyTable 配置实现跨站api代理访问

新H5应用项目欲实现前后端完全分离,脱离原来的后端框架,独立工程进行开发。Auth相关可以通过cookie共享解决,但接口访问就会涉及跨域,在尝试cors方案各种坑后,决定使用反向代理。至于代理方案,生产环境使用 nginx 可以轻松实现,但在开发阶段使用的是 webpack-dev-server,也需要支持代理访问接口。webpack官网文档并没有给出关于代理的详细配置,但其代理使用的是http-proxy-middleware,内部调用的是 http-proxy包予以实现代理。在 http-proxy-middleware 文档中有关于 proxyTable 的详细配置。对于一般场景来说,主要是以下两个配置:

  • changeOrigin
    设置为 true 可修改转发header中host值为 target 的URL,对于使用多虚拟主机配置的服务器可以访问对应 host,也就是目标代理服务器,如果你的目标服务器不存在多虚拟机的情况,并且后端应用逻辑无需校验指定请求的URL,也可以忽略不设置

  • pathRewrite
    重写请求url,保持与目标代理服务器的请求地址一致

    1
    2
    3
    4
    5
    6
    7
    '/api/*': {
    target: 'http://target.com/',
    changeOrigin: true,
    pathRewrite: {
    '^/api' : ''
    }
    }

经过rewrite后 /api/users/info ->/users/info,如果不rewrite,目标地址会加上 /api 前缀,不符合目标服务器的url请求规

使用 Webpack 4 和 Babel 7 配置 Vue.js 工程模板

团队最近开始一个新项目,技术栈采用 Vue.js 2 框架。由于是一个较复杂的大型单页应用,决定使用官方推荐的 webpack 工程作为应用工程脚手架。该工程模板中使用了 webpack 3 和 babel 6,由于 webpack 4 已发布,Babel 7(beta) 也出来挺长一段时间了,所以想对工程模板进行升级改造,以尝试工程化开发工具的最新特性。

关键调整

webpack工程模板本身是为大型应用设计的,构建脚本和配置文件的结构相比一般项目稍显复杂,添加了一些开发辅助工具,并且做了一定程度的封装。本次升级也是沿用了此套结构,只是对升级后的几个工具配置项做了调整。几个关键调整:除了 webpack 4 和 Babel 7,配套的 vue-loader 也升级到了 v15(官方的文档已经是v15了);另外为了配套 webpack 4,extract-text-webpack-plugin 插件也被替换成了mini-css-extract-plugin,并且配合 css-hot-loader
可实现提取css文件的热替换。主要配置项调整请看下文,前后配置代码差异可以见这里,完整工程模板库地址:https://github.com/xsbear/vue-webpack4-babel7,有需要的同学可以直接 clone。

基础配置 build/webpack.base.conf.js

这里的主要变化是新的 vue-loader 配置,由于新版配置选项发生变更,原来vue-loader.conf.js里的配置项基本已废弃,本方案直接将transformAssetUrls选项配置在了 vue-loader 的 rule options 里;另外需引入vue-loader/lib/plugin,以确保正确解析 .vue文件 <script>块中的js代码。

开发模式配置 webpack.dev.conf.js

webpack 4 的一个主要变化: 添加 mode 参数,值为 development,移除已废弃的NamedModulesPluginNoEmitOnErrorsPlugin 等插件,另外引入 mini-css-extract-plugin 作为css文件提取插件。当然,如果你不打算在开发模式下使用css提取(使用 style-loader 替代),也可以不引入此插件,且在调用utils.styleLoaders方法配置css模块规则时,extract 参数传 false

#build/utils.js

对 utils里的 generateLoaders 方法做了调整,替换css提取插件,增加 hotReload 参数,供开发、生产模式选择是否要执行css模块热加载。此方法可以根据传入的 usePostCSS 参数或 loader 参数组装成自定义的css预处理或后处理的 loader 组合,结合 styleLoaders 方法再组装完整的css模块rules,如 .css, .postcss, .sass 等。cssLoaders 方法返回中,枚举了多种css预处理格式,实际项目中可根据自己采用的预处理方案进行选择,无需全部配置,如只使用postcss,后面几个预处理格式都可以注释。

生产模式配置 build/webpack.prod.conf.js

这里的主要调整是为适配 webpack 4 的新配置。同样的,新增参数 mode: ‘production’;比较大的调整是 optimization 配置,替代了之前的 CommonsChunkPlugin, UglifyJsPlugin 等插件。另外如 HashedModuleIdsPlugin 等插件也已废弃,无需再配置。这里的重点是替代 CommonsChunkPlugin 插件的 splitChunks 配置项,目前给出的配置是可以满足 vendor 库和业务公用代码库分离的典型场景,更多分离优化的配置,有需要的同学可查找相关教程,由于不是本文重点,这里就不展开了。

Babel 7 配置 .babelrc

Babel 7 的相关依赖包需要加上 @babel scope。一个主要变化是 presets 设置由原来的 env 换成了 @babel/preset-env, 可以配置 targets, useBuiltIns 等选项用于编译出兼容目标环境的代码。其中useBuiltIns如果设为 "usage",Babel 会根据实际代码中使用的ES6/ES7代码,以及与你指定的targets,按需引入对应的 polyfill,而无需在代码中直接引入 import '@babel/polyfill',避免输出的包过大,同时又可以放心使用各种新语法特性。

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
["@babel/preset-env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
},
"useBuiltIns": "usage"
}]
],
"plugins": ["transform-vue-jsx"]
}

transform-runtime

对于开发应用来说,直接使用上述的按需 polyfill 方案是比较方便的,但如果是开发工具、库的话,这种方案未必适合(babel-polyfill 是通过向全局对象和内置对象的prototype上添加方法实现的,会造成全局变量污染)。Babel 提供了另外一种方案 transform-runtime,它在编译过程中只是将需要polyfill的代码引入了一个指向 core-js 中对应模块的链接(alias)。关于这两个方案的具体差异和选择,可以自行搜索相关教程,这里不再展开,下面提供一个 transform-runtime的参考配置方案。

  • 首先安装 runtime 相关依赖

    1
    2
    npm install --save-dev @babel/plugin-transform-runtime
    npm install --save @babel/runtime
  • plugin-transform-runtime 的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "presets": [
    ["@babel/preset-env", {
    "modules": false,
    "targets": {
    "browsers": ["chrome 65"]
    }
    }]
    ],
    "plugins": ["transform-vue-jsx", ["@babel/plugin-transform-runtime", {
    "useBuiltIns": true
    // "polyfill": false
    }]
    ]
    }

这里解释一下 useBuiltIns(默认为 false) 和 polyfill(默认为 true) 两个选项,如果不配置或使用默认值,那 Babel 是会自动 polyfill 的(引入core-js polyfill 的alias),哪怕你指定的 targets 只有最新版chrome,最终编译出来的包会比较大,这不是我们所希望的。解决办法是通过配置 polyfillfalseuseBuiltInstrue (二选一即可),从而不引入 core-js 的 polyfill 。如果目标环境是较新的浏览器,可以按此配置,但是有一些最新的特性可能还需要单独 polyfill。个人认为,如果开发应用可以直接使用 babel-polyfill 方案,相对来说简单方便,编译输出的包大小也是可控的。

其它

原工程中有测试模块,引入了相关依赖包。此次升级,为适配 webpack 4 和 babel 7 也调整了相关依赖及配置。如果你项目中不需要引入测试模块,可以考虑将以下依赖包移除:

1
2
3
4
5
6
7
8
9
10
11
12
"@babel/register"
// 这个是 jest 为兼容 Babel 7 的一个依赖,注意不是 @babel/core
"babel-core": "^7.0.0-bridge.0"
"babel-jest"
"regenerator-runtime"
"chromedriver"
"selenium-server"
"jest"
"jest-serializer-vue"
"nightwatch"
"selenium-server"
"vue-jest"

其他的一些调整如 postcss.config.js 等,可自行查看相关差异,不再赘述。

总结

webpack 工程作为一个针对大型应用设计的官方通用模板,为了兼容并包及开箱即用对工程配置做了一定程度的封装及开发辅助工具的整合,加快了项目开发的上手速度,但也对编译、构建工具的升级调整带来了一些不便。具体到实际项目中,可以根据情况对 webpack 的相关配置进行简化,比如只保留 webpack.base.conf, webpack.dev.confwebpack.prod.conf 3个配置文件的结构即可,便于维护。

本人对于 webpack,Babel 等工具的掌握也仅限于应用层面,只是想把此次摸索的成果与大家进行分享,如描述有误或不到位的地方,欢迎指正交流。

参考:
Setting up a Vue.js Project with webpack 4 and Babel 7
Upgrade to Babel 7