在使用webpack打包vue项目时,我们一般习惯以项目的main.js
为打包入口,构建完整的项目依赖。依赖构建完毕后将打包后的js通过插件html-webapck-plugin
引入到HTML模板中,从而实现整个项目的打包。
由于webpack运行于nodejs环境下,只能识别js文件,因此如果遇到其他类型的文件,如.vue
,就必须使用对应的loader
先转化成js文件。Vue提供了vue-loader
来将vue单页面组件转化成js代码(vue-loader
本身依赖vue-template-compiler
来解析模板,如果组件包含样式,一般还需要引入style-loader
和css-loader
)。
可能很多人会有一个错觉,是不是webpack只能以js文件为入口?
当然不是这样的!只要有loader
的支持,.vue
文件同样可以作为打包入口。这也是我们打包单个vue组件的基本思路。下面我们来看具体的打包过程。
这个非常简单,首先安装webpack和webpack-cli(高版本webpack依赖webpack-cli):
npm install webpack webpack-cli -g
然后进入要建项目的目录,新建一个文件夹(如vue-pack
),打开命令行输入以下指令初始化一个项目:
npm init
一路回车,最终会在该路径下自动生成一个package.json
,这是项目描述文件。由于当前项目还没有安装任何依赖,所以package.json
的dependencies
字段还不存在。所以接下来我们要在当前项目中安装打包单个vue组件所需要的包(高版本的vue-loader
需要依赖vue-loader-plugin
插件,否则打包的时候会报错):
npm install vue-loader vue-loader-plugin vue-template-compiler vue-style-loader css-loader --save
回车,等待安装完成。现在项目下应该会增加一个node_modules
文件夹,这是第三方node包目录。
由于webpack4不再默认生成配置文件,因此我们可以在当前路径下新建一个webpack.config.js
文件作为打包的配置文件。
由于我们只是简单演示单组件的打包,这里以一个简单的Vue组件为例。我们在当前目录下新建src/HelloWorld.vue
,然后编写简单的vue组件:
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
data() {
return {
name: 'carter'
}
}
}
</script>
<style>
div {
color: red;
}
</style>
现在最关键的是如何配置打包脚本,我们来看webpack.config.js
的编写:
webpack.config.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
entry: './src/HelloWorld.vue',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'helloWorld.js',
library: 'helloWorld'
},
plugins: [
new VueLoaderPlugin()
],
module: {
rules: [{//vue 解析
test: /\.vue$/,
loader: 'vue-loader',
},
{//css 解析
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
}]
}
}
我们来解释上述配置。
首先,entry字段定义了打包入口为src路径下的HelloWorld.vue
,也就是我们将要打包的vue组件。这里的library
字段我们在后面会解释。
然后,output定义了打包出口,这里我们会把打包结果输出到当前路径下的dist
文件夹,打包结果名为helloWorld.js
。
随后,我们在plugins中引入了VueLoaderPlugin
,这是vue-loader
所需的依赖。
最后,我们对不同的资源配置loader
。对于.vue
后缀的,我们使用vue-loader
解析,对于样式,我们使用vue-style-loader
和css-loader
进行解析。
现在我们来解释一下entry
字段的library
值的含义。
添加library字段表明这个入口文件打包之后会作为一个第三方库使用,而不是会被插件嵌入到HTML模板。一般来说,文件在打包完成之后不会向外输出变量,而是得到一个立即执行的函数,如:
!(function(){ ... })()
这样的打包结果会被html-template-compiler
嵌入到模板中调用:
...
<script src="/js/bundle.js"></script>
...
由于我们的打包入口一般都是main.js,它内部包含new Vue这样的代码,因此由它打包出的js具有自动挂载和初始化的能力,我们只需简单引入它,就可以构建整个vue应用。
但是单vue组件不同,它只是第三方包,没有new Vue这样的语句,不具备自动挂载的能力,需要在引入到项目后手动注册和挂载。因此单vue组件的输出结果必须是一个函数或对象。指定了library字段后,打包结果就变成了:
var helloWorld=function(e){...};
这个函数严格来讲是一个Module
对象,它有一个default属性,保存了组件配置。现在你就可以通过变量helloWorld
来执行组件的注册和挂载了,这个我们第三部分会介绍。
编写完脚本之后,打包就很容易了。我们去package.json
里配置打包脚本:
{
...
"scripts": {
...
"build": "webpack",
},
...
}
现在打开命令行,输入npm run build
,你就会发现当前路径下多了一个dist文件夹,里面的helloWorld.js
就是我们的打包结果。
现在假设我们另外有一个项目,需要用到上述组件。而上述打包结果现在已经被保存在当前项目的public文件夹下,我们现在需要在App.vue中动态加载这个js,然后注册和使用组件。如:
public
|- js
|- helloWorld.js
src
|- App.vue
|- main.js
App.vue可能会这样写:
<template>
<div :is="compName"></div>
<template>
<script>
import Vue from 'vue';
export default {
data() {
return {
compName: ''
}
},
mounted() {
...
}
}
</script>
我们的目标是在mounted
生命周期中挂载这个第三方组件。假设我们还不知道要加载的第三方组件的名字(这里指的其实是打包时的library
字段)和路径,我们可能需要先发送一个请求来获取:
mounted () {
this.$axios.get('/getLibraryNameAndUrl').then(res => {
let compName = res.libraryName;
let url = res.url;
...
})
}
接下来需要动态加载js,并在加载完毕后动态注册它:
mounted () {
this.$axios.get('/getLibraryNameAndUrl').then(res => {
let compName = res.libraryName;
let url = res.url;
let script = document.createElement('script');
script.src = url; // '/js/helloWorld.js'
script.onload = () => {
Vue.component('hello-world', window[compName].default);
this.compName = 'hello-world';
}
document.body.appendChild(script);
})
}
现在页面上应该可以渲染出引入的第三方组件了。我们看看这里做了什么。
首先,我们动态创建了一个script标签,并将url设置为第三方helloWorld.js
的地址。然后我们注册了script标签的onload事件。我们第二部分已经讲到,helloWorld.js
返回的实际上是下面的代码:
var helloWorld= function(e){...};
也就是说,当脚本执行完毕时,window对象上应该会新增一个变量:window.helloWorld
。它的值是一个函数,同时它也是一个Module
类型的对象。在浏览器输出helloWorld
这个变量:
它的default属性就是我们要拿来进行注册组件的对象!我们通过Vue.component动态注册一个名为hello-world
的组件(这里也可以选择动态从后台获取,或其他约定好的组件名),传入上述变量的default属性,即可完成注册。
随后我们只需要写this.compName = 'hello-world'
,template中的div就会被渲染为组件
,第三方组件也就渲染完毕。
实际上在普通项目中也是一样的用法。
比如我们有一个普通的HTML页面,它引入了vue的cdn库。如果只是静态引入这个组件,那么可以这样写:
...
<div id="app"></div>
...
<script src="/js/helloWorld.js"></script>
<script>
var app = new Vue({
el: "#app",
components: {helloApp: helloWorld.default},
render: h => h('helloApp')
})
</script>
如果需要动态加载,那么完全参照vue中的代码,使用document.createElement
动态创建脚本标签并进行注册。
实际上上述的打包过程完全可以使用node服务自动化完成,并且可以动态指定组件名,以防止多个组件出现打包冲突。比如我们可以通过nodejs提供的命令行模块:node-cmd
,自动执行webpack
命令。如果需要指定library字段,可以这样动态拼写:'webpack --output-library' + libraryName
。
所以你完全可以在服务端构建一个node服务,以单vue组件作为输入,自动执行打包命令,然后输出打包结果,再引入其他项目中。关于这套方案,目前本人仍在实践中…