问题描述
今天有童鞋在用 vue 项目写代码的时候,问我为啥会出现以下报错?
[Vue warn]: 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.
(found in )
问这个问题的童鞋估计是没怎么看过 vue 的源码,vue 官网是没有这一部分的介绍的,得自己看源码!
问题原因
由于项目中引用的是 vue.runtime.esm.js
,然后在代码中又用了 template
属性,导致无法解析 template
里面内容,项目报错。
解决方案
针对不同脚手架可以这样做:
vue-cli 脚手架创建的项目
创建或修改 vue.config.js
文件,修改 webpack
配置,把项目中的 vue 换成 “编译 + 运行” 版本。
module.exports = {
...
chainWebpack: config => {
config.resolve.alias
.set("vue$",'vue/dist/vue.esm.js');
...
}
};
普通 Webpack 项目
修改一下 webpack
配置,把项目中的 vue 换成 “编译 + 运行” 版本。
module.exports = {
// ...
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
}
}
Rollup 项目
修改Rollup
配置,把项目中的 vue 换成 “编译 + 运行” 版本。
const alias = require('rollup-plugin-alias')
rollup({
// ...
plugins: [
alias({
'vue': 'vue/dist/vue.esm.js'
})
]
})
浏览器
直接 CDN
引用 UMD
类型的 “编译 + 运行” 版本。
ok!下面我们结合 Demo 分析一下 vue 的各个 dist(资源包)之间的区别。
开始
为了更好的去分析 vue 各个资源包之间的区别,我们简单的搭建一个 vue 项目。
首先创建一个目录叫 vue-dist-demo
,然后初始化 npm
:
mkdir vue-dist-demo && cd vue-dist-demo && npm init
接下来我们需要安装 webpack 相关的依赖:
安装 webpack
webpack 核心库。
在工程目录 vue-dist-demo
执行以下命令安装 webpack:
npm install -D webpack --registry https://registry.npm.taobao.org
安装 webpack-cli
webpack 指令库。
在工程目录 vue-dist-demo
执行以下命令:
npm install -D webpack-cli --registry https://registry.npm.taobao.org
安装 webpack-dev-server
webpack 开发者服务框架。
在工程目录 vue-dist-demo
执行以下命令:
npm install -D webpack-dev-server --registry https://registry.npm.taobao.org
安装 webpack-chain
webpack 配置工具。
在工程目录 vue-dist-demo
执行以下命令:
npm install -D webpack-chain --registry https://registry.npm.taobao.org
创建 webpack 配置
在工程目录 vue-dist-demo
下创建一个 webpack.config.js
文件:
touch webpack.config.js
然后对 webpack.config.js
进行配置,用 webpack-chain
导入一个 webpack 配置:
const config = new (require('webpack-chain'))();
module.exports = config.toConfig();
为了开发方便,我们在 package.json
中声明两个脚本 build
跟 dev
:
{
"name": "vue-dist-demo",
"version": "1.0.0",
"description": "## 问题描述",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rimraf dist && webpack --mode=production",
"dev": "webpack-dev-server --mode=development --progress"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.4.0",
"webpack-dev-server": "^3.11.0",
"webpack-chain": "^6.5.1",
"webpack-cli": "^3.3.12"
}
}
入口与出口
我们首先在工程目录 vue-dist-demo
下创建一个 src
目录,然后在 src
目录下创建一个 main.s
文件:
mkdir src && cd src && touch main.js && cd ..
然后我们找到 webpack.config.js
文件,对 webpack 的入口和出口进行配置:
const path = require('path');
const config = new (require('webpack-chain'))();
config
.context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
.entry('app') // 入口文件名称为 app
.add('./src/main.js') // 入口文件为 ./src/main.ts
.end()
.output
.path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
.filename('[name].[hash:8].js') // 打包出来的 bundle 名称为 "[name].[hash:8].js"
.publicPath('/') // publicpath 配置为 "/"
.end()
安装 vue
vue 核心 API。
npm install vue --registry https://registry.npm.taobao.org
安装 vue-loader
.vue
文件加载器。
npm install vue-loader -D --registry https://registry.npm.taobao.org
安装 vue-template-compiler
.vue
文件模版解析器。
npm install vue-template-compiler -D --registry https://registry.npm.taobao.org
安装 html-webpack-plugin
npm install -D html-webpack-plugin -D --registry https://registry.npm.taobao.org
接下来我们在工程目录 sy_webpack-wedding
底下创建一个 public
目录,然后在 public
目录下创建一个 index.html
文件作为我们 app 的入口页面:
mkdir public && touch public/index.html
然后将以下内容写入 public/index.html
:
Title
webpack 配置全部内容:
const path = require('path');
const config = new (require('webpack-chain'))();
config
.context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
.entry('app') // 入口文件名称为 app
.add('./src/main.js') // 入口文件为 ./src/main.ts
.end()
.output
.path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
.filename('[name].[hash:8].js') // 打包出来的 bundle 名称为 "[name].[hash:8].js"
.publicPath('/') // publicpath 配置为 "/"
.end()
.resolve
.extensions
.add('.js')
.add('.vue') // 配置以 .js 等结尾的文件当模块使用的时候都可以省略后缀
.end()
.end()
.module
.rule('vue') // vue-loader 相关配置
.test(/\.vue$/) // 匹配 .vue 文件
.use('vue-loader')
.loader('vue-loader')
.end()
.end()
.end()
.plugin('vue-loader-plugin') // vue-loader 必须要添加 vue-loader-plugin
.use(require('vue-loader').VueLoaderPlugin, [])
.end()
.plugin('html') // 添加 html-webpack-plugin 插件
.use(require('html-webpack-plugin'), [
{
template: path.resolve(__dirname, './public/index.html'), // 指定模版文件
chunks: ['app'], // 指定需要加载的 chunk
inject: 'body', // 指定 script 脚本注入的位置为 body
},
])
.end()
.devServer
.host('0.0.0.0') // 服务器外部可访问
.disableHostCheck(true) // 关闭白名单校验
.contentBase(path.resolve(__dirname, './public')) // 设置一个 express 静态目录
.historyApiFallback({
disableDotRule: true, // 禁止在链接中使用 "." 符号
rewrites: [
{ from: /^\/$/, to: '/index.html' }, // 将所有的 404 响应重定向到 index.html 页面
],
})
.port(8080) // 当前端口号
.hot(true) // 打开页面热载功能
.sockPort('location') // 设置成平台自己的端口
.open(true);
module.exports = config.toConfig();
测试
我们在 src
目录下创建一个 app.vue
文件:
touch src/app.vue
然后将以下内容写入其中:
{{ msg }}
很简单,就是一个简单的 vue 文件。
然后在 main.js
中引入 app.vue 文件:
import Vue from 'vue';
import App from './app.vue';
new Vue({
el: '#app',
render: (h) => h(App),
});
ok!一切准备完毕后,我们直接在工程目录运行 npm run dev
命令:
npm run dev
运行完毕后浏览器打开:
可以看到,一个简单的 vue 项目就搭建并且运行起来了。
其实如果对 webpack 很熟悉的话,从 0 开始搭建一个 vue 项目就很简单了,所以强烈推荐大家去看一下我上一篇 webpack 的文章,来和 webpack 谈场恋爱吧。
ok!我们看一下目前的 src/main.js
文件:
import Vue from 'vue';
import App from './app.vue';
new Vue({
el: '#app',
render: (h) => h(App),
});
我们试着换一种方式来实现渲染:
import Vue from 'vue';
import App from './app.vue';
new Vue({
el: '#app',
components:{
App,
},
template: " "
});
可以看到,我们注册了一个 App
组件,然后通过 vue 的 template 渲染了一个 App
组件。有点 vue 基础的童鞋,这个代码还是很容易看懂的,我就不一一解释了。
我们运行一下 npm run dev
命令,然后打开浏览器看效果:
npm run dev
可以看到,跟我们文章一开始报的错误一样了,那么我们可以怎么修改呢?前面已经给了解决方法。
-
修改
webpack.config.js
文件,把项目中的 vue 换成 “编译 + 运行” 版本。const path = require('path'); const config = new (require('webpack-chain'))(); config .context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录 .entry('app') // 入口文件名称为 app .add('./src/main.js') // 入口文件为 ./src/main.ts .end() .output .path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录 .filename('[name].[hash:8].js') // 打包出来的 bundle 名称为 "[name].[hash:8].js" .publicPath('/') // publicpath 配置为 "/" .end() .resolve .extensions .add('.js') .add('.vue') // 配置以 .js 等结尾的文件当模块使用的时候都可以省略后缀 .end() .alias .set('vue$', 'vue/dist/vue.esm.js') .end() .end() .module .rule('vue') // vue-loader 相关配置 .test(/\.vue$/) // 匹配 .vue 文件 .use('vue-loader') .loader('vue-loader') .end() .end() .end() .plugin('vue-loader-plugin') // vue-loader 必须要添加 vue-loader-plugin .use(require('vue-loader').VueLoaderPlugin, []) .end() .plugin('html') // 添加 html-webpack-plugin 插件 .use(require('html-webpack-plugin'), [ { template: path.resolve(__dirname, './public/index.html'), // 指定模版文件 chunks: ['app'], // 指定需要加载的 chunk inject: 'body', // 指定 script 脚本注入的位置为 body }, ]) .end() .devServer .host('0.0.0.0') // 服务器外部可访问 .disableHostCheck(true) // 关闭白名单校验 .contentBase(path.resolve(__dirname, './public')) // 设置一个 express 静态目录 .historyApiFallback({ disableDotRule: true, // 禁止在链接中使用 "." 符号 rewrites: [ { from: /^\/$/, to: '/index.html' }, // 将所有的 404 响应重定向到 index.html 页面 ], }) .port(8080) // 当前端口号 .hot(true) // 打开页面热载功能 .sockPort('location') // 设置成平台自己的端口 .open(true); module.exports = config.toConfig();
可以看到,我们指定 webpack 的
alias
,给vue
源码指向了vue/dist/vue.esm.js
文件,效果我就不演示了哈,小伙伴自己运行看效果。 -
引入 vue 的时候直接引入
vue/dist/vue.esm.js
文件。修改一下
src/main.js
文件:// import Vue from 'vue'; import Vue from 'vue/dist/vue.esm'; import App from './app.vue'; new Vue({ el: '#app', components:{ App, }, template: "
" }); 可以看到,我们引入 vue 的时候直接指定了
vue/dist/vue.esm.js
文件,小伙伴自己运行看效果。
分析
效果我们看到了,怎么解决报错我们也知道了,那么各个 vue 源码文件到底有什么区别呢?
我们先看一下到底有哪些 vue 源码文件?我们找到 vue 的依赖:
可以看到,vue 的源码文件还是比较多的,那么之间的区别又是啥呢?我们平时的项目又该怎么选呢?
其实官方已经给了一段解释,但是解释的很简单,我们可以找到 vue/dist/README.md
文件:
UMD CommonJS ES Module Full vue.js vue.common.js vue.esm.js Runtime-only vue.runtime.js vue.runtime.common.js > > vue.runtime.esm.js Full (production) vue.min.js Runtime-only (production) vue.runtime.min.js
其实我们发现,虽然源码文件很多,但是区别好像就是一个加了 compiler
,一个没加 compiler
,比如:vue.js
跟 vue.runtime.js
。
我们去 vue 的官网找到一份打包各个版本的脚本文件 https://github.com/vuejs/vue/blob/dev/scripts/config.js:
...
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.dev.js'),
format: 'cjs',
env: 'development',
banner
},
'web-runtime-cjs-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.prod.js'),
format: 'cjs',
env: 'production',
banner
},
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.dev.js'),
format: 'cjs',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
'web-full-cjs-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.prod.js'),
format: 'cjs',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// Runtime only ES modules build (for bundlers)
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
format: 'es',
banner
},
// Runtime+compiler ES modules build (for bundlers)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
format: 'es',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.js'),
format: 'es',
transpile: false,
env: 'development',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.min.js'),
format: 'es',
transpile: false,
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// runtime-only build (Browser)
'web-runtime-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.js'),
format: 'umd',
env: 'development',
banner
},
// runtime-only production build (Browser)
'web-runtime-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.min.js'),
format: 'umd',
env: 'production',
banner
},
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler production build (Browser)
'web-full-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.min.js'),
format: 'umd',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// Web compiler (CommonJS).
'web-compiler': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
},
// Web compiler (UMD for in-browser use).
'web-compiler-browser': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/browser.js'),
format: 'umd',
env: 'development',
moduleName: 'VueTemplateCompiler',
plugins: [node(), cjs()]
},
// Web server renderer (CommonJS).
'web-server-renderer-dev': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.dev.js'),
format: 'cjs',
env: 'development',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-prod': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.prod.js'),
format: 'cjs',
env: 'production',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-basic': {
entry: resolve('web/entry-server-basic-renderer.js'),
dest: resolve('packages/vue-server-renderer/basic.js'),
format: 'umd',
env: 'development',
moduleName: 'renderVueComponentToString',
plugins: [node(), cjs()]
},
'web-server-renderer-webpack-server-plugin': {
entry: resolve('server/webpack-plugin/server.js'),
dest: resolve('packages/vue-server-renderer/server-plugin.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-webpack-client-plugin': {
entry: resolve('server/webpack-plugin/client.js'),
dest: resolve('packages/vue-server-renderer/client-plugin.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
// Weex runtime factory
'weex-factory': {
weex: true,
entry: resolve('weex/entry-runtime-factory.js'),
dest: resolve('packages/weex-vue-framework/factory.js'),
format: 'cjs',
plugins: [weexFactoryPlugin]
},
// Weex runtime framework (CommonJS).
'weex-framework': {
weex: true,
entry: resolve('weex/entry-framework.js'),
dest: resolve('packages/weex-vue-framework/index.js'),
format: 'cjs'
},
// Weex compiler (CommonJS). Used by Weex's Webpack loader.
'weex-compiler': {
weex: true,
entry: resolve('weex/entry-compiler.js'),
dest: resolve('packages/weex-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
}
}
...
我们挑两个文件来分析一下,vue.js
跟 vue.runtime.js
。
vue.runtime.js
找到 vue.runtime.js
的脚本配置:
// runtime-only build (Browser)
// 运行时代码
'web-runtime-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.js'),
format: 'umd',
env: 'development',
banner
},
找到 vue.runtime.js
的入口文件 /web/entry-runtime.js
:
/* @flow */
import Vue from './runtime/index'
export default Vue
很简单,就直接导出了 Vue
。
vue.js
找到 vue.js
的脚本配置:
// Runtime+compiler development build (Browser)
// 运行时代码 + 编译时代码
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
找到 vue.js
的入口文件 web/entry-runtime-with-compiler.js
:
/* @flow */
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to or - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue
可以看到,相比 entry-runtime.js
,entry-runtime-with-compiler.js
重写了 Vue 的$mount
方法,在 $mount
方法里面利用了 compileToFunctions
方法把 template
编译成了 render
函数,更多 Vue 源码的解析可以参考我之前的几篇 Vue 源码的文章:
- Vue源码解析(一)
- Vue源码解析二(render&mount)
- Vue源码解析之(computed&watch&观察者模式)
其它几个源码文件大家可以按照一样的方式去分析,我就不一一解析了。
总结
如果你需要用到 vue 的 compiler 功能,你就需要用带 compiler 功能的 vue 源码,如果你不需要的话,只需要导入 runtime 版本的 vue 源码即可,因为带 compiler 功能的 vue 源码比runtime 版本的 vue 源码多了模版编译的功能,会使项目的 size 更大一些。