基于webpack手动搭建一个简单的vue脚手架
前言
首先,当我们编写代码时,不通过模块化的思想想要引入一个js,通常是在html文件中创建一个script标签,引入我们需要的js,如果我们引入的是自己手写的js,很容易发生变量重名的冲突。
例如:
// 变量同名冲突
// a.js
var a = 111 或者 let a = 111
// b.js
var a = 222 或者 let a = 222
// index.html 中同时引入a.js、b.js发生错误
所以,我们有时想通过一些方法去避免这些错误,比如:匿名函数,但匿名函数又会发生每个js文件中的作用域私有,而代码不可发生复用的问题,因此又需要想方法解决,例如:
// a.js
var moudleA = (function() {
const obj = {}
const a = 111
obj.a = a
return obj // 将匿名函数中的变量暴露出去,实现代码复用
})()
// b.js
(function() {
const a = 111
console.log(moudleA.a) // 引用a中的变量,并且解决变量重名
})()
// index.html 中同时引入a.js、b.js
并且,这种导入方式对js插入顺序依赖性很强,一般公司多人开发,不同的开发人员引入不同的js文件,插入位置错误发生的报错。
但不可否认,通过匿名函数将变量暴露出去这种方式,就是最基础的模块封装。当然现在对于前端模块化已经有了很多规范:Common.js、AMD、CMD、ES6中的Modules。
当然,这些东西我这不进行说明了,资料百度就行。
开始
1. 先创建一个项目
打包文件
2.安装和使用webpack
使用模块化思想,解决变量重名和代码打包
当然开发文件中需要安装依赖和包, 先将文件夹init一下吧。
npm init -y
文件夹是中文名的话,这样会报错,需要npm init
回车然后填写名称,其他信息一路回车即可,然后熟悉的package.json
两兄弟文件就在根目录中出来了。
现在安装webpack,我使用的是3.6.0, 现在版本4.4.x了,你想跟我一样 直接npm i webpack@3.6.0 -D
即可。
我npm安装是缩写模式, 简单说下:
-D
为 --save-dev
开发环境中使用
-S
为 --save
生成环境中使用
缩写不缩写无所谓, 不差那几个字母, 我比较懒。
顺带说下为啥在项目中安装webpack,而不是全局安装?
当你全局安装了webpack, 你使用指令
webpack
打包又先调用的是你全局的webpack。 毕竟你今天安装的webpack是这个版本,不能保证以后谁拿你代码一运行调用他电脑下载的webpack版本,beng,满屏飘红就出来了。
所以保险点,项目都安装自己的webpack。
2.1 打包
当你安装好webpack后执行打包指令后, 执行"webpack xxxx"时要注意,此时如果你安装了全局webpack,任然调用的是你全局的而不是项目自己的,你项目调用自己的webpack指令应该是./node_modules/.bin/webpack xxxxx
这时你会说 “我@#$X,很@#%烦”, 为了解决该问题,我们可以添加webpack.config.js
// webpack.config.js
const path = require("path")
module.exports = {
entry: "./main.js", // 入口
output: {
path: path.resolve(__dirname, 'dist'), // 出口。必须为绝对路径,所以借助node的path模块
filename: 'bundle.js' // 打包后文件名
},
}
// 意思就是:我根目录有个main.js文件,里面导入了好多东西,帮我打包到我目录里面的dist文件下bundle.js文件里
并且还需要更改一下pachage.json文件中的scripts属性
{
...
scripts: {
...
"build": "webpack" // build可以随便取,如: 叫xxx,终端运行 npm run xxx即可
}
...
}
// 意思: 以后我直接在终端运行一个叫build的指令去调用webpack打包, 并且通过该指令, 会自动帮我们调用开发环境中的webpack打包
当运行npm run build
时,webpack会看看我们根目录是不是有一个叫webpack.config.js的文件,然后读取运行其中的配置。
弄完这一步,最后将index.html
中引入的main.js
改为bundle.js
后,我们可以愉快的将各个js文件当成模块,各种require、import、、、,尽情导入导出的去骚了。
比如,我这里在src文件夹中新建js
文件夹,添加info.js
、mainUnit.js
然后再main.js
中引入并使用
// info.js
const name = "zou";
const age = 18;
const height = 18.8;
export default {
name,
age,
height
}
// mainUnit.js
function add(num1, num2) {
return num1 + num2
}
function mul(num1, num2) {
return num1 * num2
}
module.exports = {
add,
mul
}
// main.js
import info from './src/js/info';
const {add, mul} = require('./src/js/mainUnit.js');
console.log(add(100, 200));
console.log(mul(100, 200));
console.log(info);
此时目录结构
注意:我结构中webpack.config.js多了copy是因为后期做了抽离,webpack打包并没有调用我的webpack.config copy.js
loader的使用
webpack并不能识别许多文件类型,我们需要通过各种各样的loader,来帮webpack识别。
干完上面一步的时候, 在我们骚的还没尽兴完时。
领导一看,“我靠,流批,给我把所有的js相关,css相关,图片相关都给我弄进去, 以后我只上线你导出的这一个js文件”。
(css随便添加点东西,less文件中写些less语法)
这时,在main.js中引入
require("xxx.css")
或者import "xxx.less"
,然后一运行,发现我的终端,好红啊。
3.1引入css相关loader
进入webpack官网, 点击导航中的LOADERS,点击左边导航的样式,然后就可以根据需求对照文档安装和配置你需要的样式相关loader了
我这里安装css和less相关的loader,npm i style-loader css-loader less-loader less -D
因为loader基本用于开发环境, 所以安装一般都为--save-dev
// webpack.config.js
module.exports = {
...
rules: [
{
test: /\.css$/,
// css-loader 只负责解析css,并不负责插入样式到页面
// style-loader 负责将样式插入页面中
// 使用多个loader时,从右往左调用
use: ['style-loader','css-loader']
},
{
test: /\.less$/,
use: [{ // use中如需配置其他options 可以使用Object形式, 否则不配置可直接用Array形式,同上css-loader
loader: "style-loader"
}, {
loader: "css-loader"
}, {
loader: "less-loader"
}]
},
]
...
}
好了,css文件的问题解决了,现在继续解决图片的问题
3.2引入img相关loader
在网上随便找2张一大一小图片,随便一个引入的css文件中添加,我是在less中添加,顺便试一试less-loader,如:
@fontSize: 40px;
@fontColor: orange;
body {
//font-size: @fontSize;
//color: @fontColor;
background-image: url('../img/big.jpg');
background-size: 30%;
}
同样进入webpack官网, 点击导航中的LOADERS,找找找,蒙了!没有img相关的loader啊。那我来告诉你,去看看文件类目中的url-loader
。
话不多说,对着文档就是一顿安装,复制
// webpack.config.js
module.exports = {
...
rules: [
...
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
// 当加载图片,小于limit时,会将图片编译成base64字符串形式
// 当加载图片,大于limit时,需要安装file-loader进行加载,加载图片会放入你打包输出的文件目录
limit: 8192 // 8192 / 1024 = xKB
}
}]
}
...
]
...
}
limit属性说明如上方注释,当图片小于limit设定的值时以base64字符串形式插入在打包的js中,此时没问题。
但是,一旦大于设定值,webpack会报错,需要安装file-loader
,安装即可,不需要单独配置。
然后wenpack会将引入图片完整打包放入dist
文件夹目录下,名称为32位hash值,并将开发环境中引用的图片路径替换为打包生成的图片,而此时index.html引用的为开发目录中(即img文件夹下)的图片,所以打包后再运行index.html
发现背景图片不见了
大于limit设定值生成图片
大于limit设定值页面引用图片
所以这时我们需要当图片大于limit设定值时的打包后页面引入路径更改为
dist/xxxxx
,好的,进入webpack.config.js,在其中output
属性中添加publicPath
属性
// webpack.config.js
module.exports = {
...
output: {
...
// 添加该属性,以后打包文件所有涉及到url的东西,都会自动在路径前添加 dist/
publicPath: 'dist/'
}
...
}
添加publicPath属性打包运行后
好了,现在js模块、css相关、图片相关都可以正常被打包了。
我们通常并不希望,所有的img全部打包在dist文件夹下,而是希望放在dist/img文件夹中,并且希望图片保持原名,但怕重名,还需要加点hash值。
因此,我们可以继续配置url-loader
规则
// webpack.config.js
module.exports = {
...
rules: [
...
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192 ,
// 当不希望它打包直接放入dist文件中时,可以添加name属性,
// 当需要保存原名时可以添加[name]
// 当需要防止重名又不需要太长的hash值时可以添加[hash: x] x为你需要hash值位数
name: 'img/[name].[hash:8].[ext]'
}
}]
}
...
]
...
}
好了,打包试试看。
打包生成的图片
因为之前有publicPath
配置,所以打包运行index.html
页面图片引入路径仍然为dist/img/xxxx.xxx
ES6转ES5
完成上面一步之后,领导打开他很久没更新的浏览器,发现页面并没有出来,打开控制台一看,全是ES6语法报错,然后立马说:“你这打包不对劲啊,浏览器版本一低就各种报错,我们不能保证用户都使用可以识别ES6语法的浏览器,代码应该要将老旧浏览器都兼容啊。”
好吧,满足他的需求,打开浏览器搜索babel
,进入中文官网,点击设置
选择Webpack
,对照文档在编辑器中一顿输出。
npm install --save-dev babel-loader @babel/core @babel/preset-env
安装完成后,在webpack.config.js中添加规则:
// webpack.config.js
module.exports = {
...
rules: [
...
{
test: /\.js$/,
exclude: /node_modules/, // 排除的目录
// 使用babel-loader将ES6代码转为ES5,做浏览器兼容
// 同时需要建立.babelrc文件,调用@babel/preset-env插件将E6转为E5S
loader: "babel-loader"
}
...
]
...
}
此时,babel-loader
已经可以将ES6语法识别,但是打包将ES6转ES5还需要@babel/preset-env
插件,所以我们要新建一个名为.babelrc
的babel配置文件使用该转译插件:
// .babelrc
{
"presets": ["@babel/preset-env"]
}
大功告成,圆满收工!
此时目录结构
使用Vue
正准备执行回家程序操作,领导叫住说:“项目前端框架要用vue,就用你那玩意搞,正好可以打包。”得,继续搭vue吧。
npm i vue -S
因为vue不仅是在开发环境中使用,并且打包后依然需要依赖vue,所有安装在生成环境中。
安装之后,在main.js
中使用一下吧
// main.js
import Vue from "vue";
new Vue({
el: "#app",
data: {
msg: "哈哈,使用了Vue"
}
})
然后在index.html
中使用一下
// index.html
...
<div id="app">
<h2>{{ msg }}</h2>
</div>
...
写完后,觉得干的很漂亮,美滋滋,打包,运行index.html
,dang,在页面上没有看到哈哈,打开控制台一看,一串红,很烦。
好吧,
main.js
中引入的vue模块,使用的是runtime-only
版本的,该版本有vue运行代码,但没有编译template
的代码。那咋办,换个vue模块引入的版本呗,打开webpack.config.js。
// webpack.config.js
module.exports = {
...
// 设置模块如何被解析
resolve: {
// 当安装vue时,默认使用的是runtime-only版本,此版本只含有vue运行的代码,不包含编译template的代码
// 需要重新更换含有runtime-compiler的版本,因为runtime-complier含有complier代码可以用于编译template
// alias(别名): 用别名代替前面的路径,不是省略,而是用别名代替前面的长路径
// 如下,当main.js中import Vue from "vue"时,因为vue是别名,所以实际为import Vue from "vue/dist/vue.esm.js"
// 别名好处是webpack直接会去别名对应的目录去查找模块,减少了webpack自己去按目录查找模块的时间
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
...
}
属性解释如上图,语文好的去官方文档看,反正我是理解不了它说的啥,文档说点白话,通俗易懂不好吗。
简单说alias
就是拼接导入模块的路径,emmm,还是举例吧,如下:
// webpack.config.js
...
alias: {
'vue$': 'vue/dist/vue.esm.js' // 这是别名vue
}
...
// main.js
import Vue from "vue" <==> import Vue from "vue/dist/vue.esm.js"
import Vue from "vue/xxx/xxx" <==> import Vue from "vue/dist/vue.esm.js/xxx/xxx"
好了,应该懂了,反正也是写我自己看的,你们不理解百度去吧。
在我这其实是将alias
当成vue模块引入版本重定向,当引入vue,默认引入vue.runtime.common.js文件,而我将引入文件重定向为vue.esm.js。
我怎么知道默认引入哪个版本的?
打开node_modules/vue/package.json,查看其中main属性,就是vue模块默认引入的版本。
其他版本,在node_modules/vue/dist文件夹中。
到这一步,vue配置就完成了。
vue中template的封装
完成上一步已经可以打包运行vue,并且编译template语法了。上方没重定向之前,明明没有template语法为何打包运行会报错呢?
// vue中template语法之一
// html
...
<div id="app"></div>
<template id="demo">
<div>
这是模板语法之一
</div>
</template>
...
// js
const tmp = {
template: "#demo"
}
Vue.component("cpn", tmp)
是不是感觉很熟悉?可以理解为new vue
中el
属性通常挂载的"#app"
其实就是实例的模板。
好了,回归当前正题,一般在使用vue开发过程中,我们会发现通常只有一个html文件,称之为单页面复应用(SPA:single-page application),而在SPA中,我们不希望所有的节点全部写在那一个html文件,html中只需要一个vue挂载节点即可,并且也不希望将所有的js全部写在new Vue
中,所以需要将代码进一步的封装。
下面展示,如何将代码一步一步抽离:
1. 初始代码
// index.html
...
<div id="app">
{{ msg }}
<button @click="btnClick">点击</button>
</div>
...
// main.js
...
new Vue({
el: "#app",
data: {
msg: "哈哈,使用了Vue"
},
methods: {
btnClick() {
console.log('vue内容封装');
}
}
})
...
1.1封装一
// index.html
...
<div id="app"></div>
...
// main.js
...
new Vue({
el: "#app",
template: `
<div id="app">
{{ msg }}
<button @click="btnClick">点击</button>
</div>
`,
data: {
msg: "哈哈,使用了Vue"
},
methods: {
btnClick() {
console.log('vue内容封装');
}
}
})
...
1.2封装二
// main.js
...
new Vue({
el: "#app",
template: '<App/>',
components: {
App: {
template: `
<div id="app">
{{ msg }}
<button @click="btnClick">点击</button>
</div>
`,
data() {
return {
msg: "哈哈,使用了Vue"
}
},
methods: {
btnClick() {
console.log('vue内容封装');
}
}
}
}
})
...
1.3封装三
利用模板化思想,将组件抽离放入一个js文件中,将其暴露出来,在src文件夹新建vue文件夹中新建app.js
// app.js
export default {
template: `
<div id="app">
{{ msg }}
<button @click="btnClick">点击</button>
</div>
`,
data() {
return {
msg: "哈哈,使用了Vue"
}
},
methods: {
btnClick() {
console.log('vue内容封装');
}
}
}
// main.js
import App from './src/vue/app.js';
new Vue({
el: "#app",
template: '<App/>',
components: {
App
}
})
做完这一步,项目已经实现模块化思想,但是此时封装缺点是模板(即节点)没有和js分离,这时候需要使用.vue
文件做进一步封装
1.4封装四
在vue文件夹中创建App.vue,删除app.js(也可以保留,只有main.js里没引用就行)
// App.vue
<template>
<div id="app">
{{ msg }}
{{ devServer }}
<button @click="btnClick">点击</button>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
msg: 'hello Webpack!',
devServer: '测试webpack-dev-server插件是否生效'
}
},
methods: {
btnClick() {
console.log('vue内容封装--App.vue');
}
}
}
</script>
<style lang="scss" scoped>
</style>
将main.js中导入app.js修改成导入App.vue
// main.js
// import App from './src/vue/app.js';
import App from './src/vue/App.vue';
此时,打包项目,发现终端报错,因为webpack并不能识别.vue
文件,所以需要安装对应的loader
来帮助webpack识别.vue
文件。
npm i vue-loader vue-template-compiler -D
在webpack.config.js中配置loader规则:
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
...
module: {
rules: [
...
{
test: /\.vue$/,
loader: 'vue-loader'
}
...
]
},
plugins: [
new VueLoaderPlugin() // 因为我安装的"vue-loader"版本为"^15.7.2",需要配置此插件
]
}
...
做到这里,到时候,是不是发现已经很像vue-cli了,只剩下使用一些配置插件了。
此时目录结构
插件的使用
如果说loader是webpack的识别器的话,那么插件(plugin)就可以说是webpack的拓展器,帮助我们延伸webpack的功能
html-webpack-plugin插件
当封装之后,发现我们webpack打包之后并没有生成index.html文件,而我们需要生成html文件,所以应该安装html-webpack-plugin插件。
- html-webpack-plugin插件 可以帮我们在打包文件生成html文件 并且自动生成script标签引入我们打包的js文件
- 并且会将页面所有引入的相关链接自动指向输出的文件夹目录下问生成文件
先来安装该插件npm i html-webpack-plugin -D
,然后再到webpack.config.js中继续配置
// webpack.config.js
...
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
output: {
...
// publicPath: 'dist/'
},
plugins: [
...
new HtmlWebpackPlugin({
template: 'index.html' // 指定生成html的模板,如果不知道只会默认生成html文件插入打包后js,并没有<div di="app"></div>节点
})
]
...
}
配置此插件后就可以将其中output
的publicPath
属性注释或删除,并且index.html
中script标签引入bundle.js
这段也可以删除了。
uglifyjs-webpack-plugin插件
这个插件可以帮助我们压缩和丑化打包的js代码,当然js压缩不压缩看自己需求。
为什么不直接用webpack打包是因为vue-cli2中使用该插件打包并没有使用webpack自带的打包,安装此插件版本不用安装最新,这里只做演示所以安装的为1.1.1版本,安装最新版本会报错,需要重新查看使用文档。
先安装,npm i uglifyjs-webpack-plugin@1.1.1 -D
,然后到webpack.config.js中配置
// webpack.config.js
...
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
...
plugins: [
...
new UglifyjsWebpackPlugin(), // 压缩 丑化打包后的js
]
...
}
使用插件打包后
BannerPlugin插件
当时需要对打包文件添加版权声明时(当然对我们国家来说,你开源的就是大家的),可以使用webpack自带的BannerPlugin插件。
vue的版权声明
在国外,对版权还是很重视的,所以大公司开源的代码大多携带版权声明,和版权协议类型。
其中vue使用的是MIT协议,此协议代表作者只想保留版权,而无任何其他了限制。
曾经的react的协议并不是MIT而是BSD,该协议就是说,如果你公司以react为核心构建了项目,当有一天Facebook(react是Facebook的)公司要对你公司干点什么的话,比如下架产品,或者收费什么的,那你公司就麻烦了,具体自己可以百度查查,所以当初很多公司项目开始想用react但看到协议都放弃了,比如:百度。
题外话不好了,来使用一波BannerPlugin
// webpack.config.js
...
const webpack = require('webpack');
module.exports = {
...
plugins: [
...
new webpack.BannerPlugin('这是在js压缩后添加的版权声明')
]
...
}
注意:如果同时使用了uglifyjs-webpack-plugin和BannerPlugin,将uglifyjs-webpack-plugin放在BannerPlugin之前调用,防止把你的声明也给丑化了,那要声明干嘛。
webpack-dev-server(热更新)
webpack提供一个可选本地开发服务器,这个本地服务器基于node.js搭建,内部使用express框架,可以实现让浏览器自动刷新显示我们修改后的结果
弄完上面那些东西,是不是觉得每次修改代码都需要打包再运行很烦,现在,它(热更新)来了。
先安装npm i webpack-dev-server -D
,老规矩,在webpack.config.js中配置
// webpack.config.js
...
module.exports = {
...
output: {...},
devServer: {
contentBase: './dist', // 为哪一个文件夹提供服务,默认根目录
inline: true // 页面实时刷新
// port: 8080, // 端口号,默认8080
// 其他配置自行看文档
},
...
}
想要使用它,还需要修改项目中的package.json,在其script
属性中添加运行指令(也可以不加,直接在终端写路径去调用,同之前webpack调用)
// package.json
...
"scripts": {
"dev": "webpack-dev-server"
}
...
然后,在终端中运行指令npm run dev
,之后就可以实时查看修改代码而不用频繁去打包运行了。
webpack.config.js抽离 和 webpack-merge
在我们安装的依赖当中,有些是开发和生产时需要的依赖,有些是开发时的依赖,还有些是生产时的依赖,我们不希望每次都运行所有的依赖,所以需要对webpack.config.js进行抽离封装
在根目录新建build
文件夹,新增三个js文件,如下:
-
base.config.js
为通用配置文件 -
dev.config.js
为开发配置文件 -
prod.config.js
为生产配置文件
此时目录结构
新增文件之后
- 将
webpack.config.js
中的代码复制到base.config.js
中 - 将
webpack.config.js
中开发需要的配置抽取到dev.config.js
中 - 将
webpack.config.js
中开发需要的配置抽取到prod.config.js
中 - 删除
base.config.js
中和dev.config.js
、prod.config.js
文件重复的代码 - 删除
webpack.config.js
,也可以改个名让webpack不认识它,像我就改成webpack.config copy.js
- 安装
npm i webpack-merge -D
,帮助我们将不同环境中的配置和通用配置文件合并 - 修改
package.json
指令
// dev.config.js
const webpackBase = require("webpack-merge"); // 合并模板
const baseConfig = require("./base.config"); // 导入共同配置
module.exports = webpackBase(baseConfig, {
devServer: {
contentBase: '../dist',
inline: true
}
})
// prod.config.js
// 开发调试时需要将 共同配置合并开发配置
const webpackBase = require("webpack-merge"); // 合并模板
const baseConfig = require("./base.config"); // 导入共同配置
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin');
const webpack = require('webpack');
module.exports = webpackBase(baseConfig, {
plugins: [
new UglifyjsWebpackPlugin(),
new webpack.BannerPlugin('这是在js压缩后添加的版权声明')
]
})
// base.config.js 复制webpack.config.js代码并删除以上配置
// package.json
...
"scripts": {
"build": "webpack --config ./build/prod.config.js",
"dev": "webpack-dev-server --config ./build/dev.config.js"
}
...
自此,一个简单的vue脚手架就干完了!