从零开始学习Vue(三)

264 阅读10分钟

模块化开发

简介

问题的产生: 功能越复杂,代码量也随之增加,通常会将代码组织在多个js文件进行维护。
产生的问题: 1.全局变量重名覆盖 2. 对js文件的依赖顺序几乎是强制性的,一旦顺序出错,就会导致上述问题。

<!--aaa.js文件中-->
falg = true
<!--bbb.js文件义-->
falg = false
<!--main.js文件中-->
if(flag) {
    console.log('hello world')
}
存在全局变量重名的问题;引入js文件的顺序决定了是否能够成功打印。

我们可以使用匿名函数来解决重名问题

(function(){
    var flag = true
})()

很显然,这样做的化我们无法在其他文件中使用flag,因为flag是一个局部变量。

解决方法: 我们可以使用将需要暴露到外面的变量,使用一个模块作为出口。

步骤:

  1. 在匿名函数内部定义一个对象。
  2. 给对象添加各种需要暴露到外面的属性和方法(不需暴露的直接定义即可)。
  3. 将对象返回,并在外面使用一个ModuleA接受。
  4. 在其他文件中使用属于该模块的属性和方法。
var ModuleA = (function(){
    var obj = {};
    obj.flag = true;
    obj.myFunc = function(info) {
        console.log(info);
    }
    return obj;
})()

// main.js
if(ModuleA.falg) {
    console.log('hello world');
}

ModuleA.myFunc('hello world');

常见的模块化规范: CommonJs、AMD、CMD、ES6的Modules

了解CommonJS

模块化有两个核心:导出和导入

<!--CommonJS的导出-->
module.exports = {
    flag: true,
    test(a,b) {
        return a+b
    }
}
<!--CommonJS的导入-->
let {flag, text} = require('moduleA');
// 等同于
let _mA = require('moduleA');
let flag = _mA.flag;
let text = _mA.test;

ES6的Modules

export基本用法

export let name = 'aaa';
export let age = 18;
export let height = 1.88;
<!--导出函数或类-->
export function add(a,b) {
    return a+b;
}
export class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    run() {
        console.log('hello world');
    }
}

// 等同于
let name = 'aaa';
let age = 18;
let height = 1.88;
function add(a,b) {
    return a+b;
}
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    run() {
        console.log('hello world');
    }
}
export {name, age, height, add, Person}

export default

某些情况下,一个模块中包含某个功能,希望由导入者自己命名,这个时候就可以使用export default.

export default function() {
    console.log('hello world');
}

// 对应的导入
import myFunc from './info.js';
myFunc()

注意: export default在同一个模块中只允许存在一个.

import使用

使用export指令导出了模块对外提供的接口,通过import命令来加载对应的模块.

import {name, age, height, add, Person} from './info.js';
// 可以通过*导入模块中所有的export,通过as起别名,方便后续使用.
import * as info form './info.js';
console.log(info.name, info.age, info.height);

Webpack

初始Webpack

从本质上来讲,webpack是一个现代的JavaScript应用的静态模块打包工具.

模块: webpack其中一个核心就是让我们可以进行模块化开发,并且帮助我们处理模块间的依赖关系.不仅JavaScript文件,包括CSS,图片,json文件等在webpack中都可以被当作模块来使用.

打包: 将webpack中的各种资源模块进行打包并合成一个或多个包(Bundle),并且在打包的过程中,可以对资源进行处理,比如压缩图片,将scss转为css,将ES6语法转成ES5语法,将ts转成js等操作.

和grunt/gulp的对比

grunt/gulp的核心是Task

  • 我们可以配置一系列的task,并且定义task要处理的事务(将scss转为css,将ES6语法转成ES5语法,将ts转成js)
  • 让grunt/gulp依次执行这些task,而且让整个流程自动化
  • 所以grunt/gulp也被称为前端自动化任务管理工具.

grunt/gulp和webpack的区别:

  • grunt/gulp更加强调的是前端流程的自动化,模块化不是它的核心.
  • webpack更加强调模块化开发管理,而文件压缩合并,预处理等功能是其附带的功能.

webpack的安装

注意: 指定版本号为3.6.0 因为vue cli2依赖该版本,后续所有操作报错大部分原因都是由于版本不一致导致的

  • 安装webpack首先需要安装Node.js, Node.js自带了软件包管理工具npm.
  • 查看自己的node版本
    node -v
  • 全局安装webpack
    npm install webpack@3.6.0 -g
  • 局部安装webpack (--save-dev 是开发时依赖,项目打包后不需要继续使用)
    cd 对应目录
    npm install webpack@3.6.0 --save-dev

全局安装后还需要局部安装的原因:

  • 在终端直接执行webpack命令,使用的是全局安装的webpack
  • 在package.json中定义了scripts时,其中包含了webpack命令使用的是局部webpack.

webpack起步

准备工作

创建如下文件和文件夹:

  • dist文件夹: 用于存放之后打包的文件
  • sec文件夹: 用于存放我们写的源文件
    • main.js: 项目的入口文件
    • mathUtils.js: 定义了一些数学工具函数,可以在其他地方引用并使用.
  • index.html: 浏览器打开展示的首页html
  • package.json: 通过npm init生成的,npm包管理的文件

mathUtils.js文件中的代码:

function add(num1, num2) {
    return num1 + num2;
}

function mul(num1, num2) {
    return num1 * num2;
}

module.exports = {
    add,
    mul
}

main.js文件中的代码:

const math = require('./mathUtils')

console.log('Hello webpack');
console.log(math.add(10, 20));
console.log(math.mul(10, 20));

js文件的打包

此时我们在js文件中使用了模块化的方式进行开发,它们不能直接使用,而如果直接在index.html中引入这两个js文件,浏览器并不识别其中的模块化代码.另外,在真是项目中存在多个js文件需要一个个引用会非常麻烦,并且不利于管理和维护.

这时候我们就需要使用webpack工具对多个js文件进行打包.
webpack src/mainjs dist/bundle.js

打包后会在dist文件夹下,生成一个bundle.js文件,它是webpack处理了项目直接文件依赖后生成的一个js文件,我们只需要将这个js文件在index.html中引入即可.

<script src="./dist/bundle.js"></script>

webpack配置

入口和出口

如果每次使用webpack的命令打包都需要写上入口和出口作为参数就非常麻烦,所以我们可以将这两个参数写到配置中,在运行时直接读取.

可通过创建配置文件webpack.config.js实现:

const path = require('path');

module.exports = {
    <!--入口: 可以是字符串/数组/对象, 这里我们入口只有一个,所以写一个字符串即可.-->
    entry: './src/main.js',
    <!--出口: 通常是一个对象,里面至少包含两个重要属性,path和filename-->
    output: {
        path: path.resolve(__dirname, 'dist'),  // 注意: path通常是一个绝对路径
        filename: 'bundle.js'
    }
}

另外: 因为一个项目往往依赖特定的webpack版本,全局版本可能和这个项目的webpack版本不一致,导致打包出现问题,所以通常一个项目都有自己局部的webpack.

package.json中定义启动

我们可以在package.json的scripts中定义自己的执行脚本.

{
    "name": "meetwebpack",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "script": {
        "build": "webpack"
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "webpack": "^3.6.0"
    }
}

package.json中的scripts的脚本在执行时,会按照一定的顺序寻找对应的位置.

  • 首先,会寻找本地的node_modules/.bin路径中对应的命令
  • 如果没找到,回去全局的环境变量中寻找.

通过npm run build执行build指令.

loader的使用

loader时webpack中一个非常核心的概念.

通过给webpack拓展对应的loader可以实现加载css,图片,ES6转ES5,ts转js,scss和less转css,.jsx和.vue文件转js文件等功能.

使用步骤:

  • 通过npm安装需要使用的loader
  • 在webpack.cofig.js中的modules关键字下进行配置

css文件处理

在项目开发的过程中,我们必然需要添加很多的样式,而样式我们往往写到一个单独的文件中.所以为样式创建一个撰文的css文件夹. normal.css文件中的代码:

body {
    background-color: red;
}

处理步骤:

  • 在入口文件main.js中引用
const math = require('./mathUtils')

console.log('Hello webpack');
console.log(math.add(10, 20));
console.log(math.mul(10, 20));

<!--必须在这里引用一次css文件-->
require('./css/normal.css')
  • 下载css-loader和style-loader (css-loader只负责加载css文件,style-loader负责将css具体样式嵌入到文档中)
    npm install --save-dev css-loader``npm install --save-dev style-loader
  • 在webpack.config.js中进行配置
const path = require('path');

module.exports = {
    <!--入口: 可以是字符串/数组/对象, 这里我们入口只有一个,所以写一个字符串即可.-->
    entry: './src/main.js',
    <!--出口: 通常是一个对象,里面至少包含两个重要属性,path和filename-->
    output: {
        path: path.resolve(__dirname, 'dist'),  // 注意: path通常是一个绝对路径
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
            }
        ]
    }
}

注意: webpack在读取使用的loader的过程中,是按照从右向左的顺序读取的。

less文件处理

webpack同样可以帮我们处理less,scss,stylus文件. 处理步骤:

  • 在入口文件main.js中引用
<!--引入less-->
require('./css/special.less')

<!--为了查看less生效的代码,添加一个div-->
document.writeln('<div>Hello World</div>')
  • 下载less-loader
    npm install --save-dev less-loader
  • 修改配置文件,添加一个rules选项,用于处理.less文件
{
    test: /\.less$/,
    use: [{
        loader: "style-loader"  // cretes style nodes from JS strings
    }, {
        loader: "css-loader"    // translates Css into CommonJS
    }, {
        loader: "less-loader"   // compiles Less to CSS
    }]
}

图片文件处理

首先,在项目中引入两张图片--test01.jpg(小于8kb)和test02.jpeg(大于8kb). 考虑在css样式中引用图片的情况,更改normal.css中的样式:

body {
    background-color: red;
    background: url(../imgs/test01.jpeg)
}

处理步骤:

  • 下载url-loader
    npm install --save-dev url-loader
  • 修改配置文件,添加一个rules选项
{
    test: /\.(png|jpg|gif|jpeg)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 8192
        }
    }]
}

limit属性的作用: 当图片小于设定的大小(8192)时,对图片进行base64编码,所以背景图片时通过base64显示出来的.
如果图片大于设定的大小(8192)时,就需要通过file-loader处理.所以当我们将background的图片改为test02.jpg就需要下载file-loader.

  • 下载file-loader
    npm install --save-dev file-loader

再次打包后,就会发现dist文件夹下多了一个图片文件. 可以发现webpack自动生成了一个32位的hash值,目的时防止名字重复.但是在真是的开发中,我们可能对打包的图片名字和存放路径有一定的要求.
所以我们可以在options中添加如下选项:

  • img: 文件要打包到的文件夹
  • name: 获取图片原来的名字放在该位置
  • hash:8: 为了防止图片名称冲突,依然使用hash,但是我们只保留8位.
  • ext: 使用图片原来的扩展名
options: {
    limit: 8192,
    name: 'img/[name].[hash:8].[ext]'
}

默认情况下,webpack会将生成的路径直接返回给使用者,但是我们整个程序的打包在dist文件夹下,所以我们还需要再路径下再添加一个'dist/'.

output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: 'dist/'
}

ES6语法处理

将ES6的语法转成ES5,需要使用babel 处理步骤:

  • 下载babel对应的loader
    npm install --save-dev babel-loader@7 babel-core babel-preset-es2015
  • 修改配置文件,添加一个rules选项
{
    test: /\.m?js$/,
    exclude: /(node_modules|bower_components)/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: ['es2015']
        }
    }
}

配置vue

首先我们需要在项目中安装Vue依赖.因为在实际项目中也会使用Vue,所以不是开发时依赖.npm install vue --save index.html

<body>
    <div id="app">
        {{message}}
    </div>
</body>

main.js

import Vue from 'vue'

new Vue({
    el: '#app',
    data: {
        message: 'hello world'
    }
})

打包运行后发现浏览器中有报错: 这个错误说的是项目使用的是runtime-only版本的Vue. Vue不同版本构建

解决方案: 修改webpack的配置,添加如下内容即可:

resolve: {
    alias: {
        'vue$': 'vue/dist/vue.esm.js'
    }
}
el和template

el用于指定Vue要管理的DOM,可以帮助解析其中的指令,事件监听等,但如果Vue实例中同时指定了template,那么template模板中的内同会替换掉挂载的对应el的模板.这样做的好处是我们不需要在以后的开发中再次操作index.html,只需要在template中写入对应的标签即可.

new Vue({
    el: '#app',
    template: `
    <div id="app">
        {{message}}
    </div>`,
    data: {
        message: 'hello world'
    }
})
Vue组件化开发引入

第一步抽取: 将template相关代码抽取出来;

// main.js
const App = {
    template: '<div id="app">{{message}}</div>',
    data() {
        return {
            message: 'hello world'
        }
    }
}

import Vue from 'vue';
new Vue({
    el: '#app',
    template: '<App />',
    components: {
        App
    }
})

第二步抽取: 我们可以将Vue相关的代码抽取到一个js文件中,并导出.

// app.js 导出
export default {
    template: '<div id="app">{{message}}</div>',
    data() {
        return {
            message: 'hello world'
        }
    }
}


// main.js 引入
import Vue from 'vue';
import App from './vue/app.js'
new Vue({
    el: '#app',
    template: '<App />',
    components: {
        App
    }
})

但是一个组件以一个js对象的形式进行组织和使用的时候是非常不方便的:

  • 一方面编写template模块非常的麻烦
  • 另一方面样式应该写在哪里?

所以以.Vue文件来组织一个vue组件,实现模板,js,样式分离.

// App.vue
<template>
    <div id="app" class="title>
        {{message}}
    </div>
</template>
<script>
    export default {
        name: 'App',
        data() {
            return {
                message: 'hello world'
            }
        }
    }
</script>
<style scoped>
    .title {
        color: blue
    }
</style>

此时,依旧需要安装对应的loader才能被正确加载 npm install vue-loader vue-template-compiler --save-dev

修改webpack.config.js的配置文件:

{
    test: /\.vue$/,
    use:['vue-loader']
}

小用例

// 子组件 Cpn.vue
<template>
    <div id="app" class="title>
        我是子组件title
    </div>
</template>
<script>
    export default {
        name: 'Cpn',
        data() {
            return {
                message: 'hello world'
            }
        }
    }
</script>
<style scoped>
    .title {
        color: green
    }
</style>

// 父组件 App.vue
// App.vue
<template>
    <div id="app" class="title>
        {{message}}
    </div>
    <Cpn></Cpn>
</template>
<script>
    import Cpn from './Cpn.vue'
    export default {
        name: 'App',
        data() {
            return {
                message: 'hello world'
            }
        },
        components: {
            Cpn
        }
    }
</script>
<style scoped>
    .title {
        color: blue
    }
</style>

// main.js
import Vue from 'vue';
import App from './vue/App.vue'
new Vue({
    el: '#app',
    template: '<App />',
    components: {
        App
    }
})

可以通过配置webpack.config.js来省略导入文件的后缀名

resolve: {
    extensions: ['.js', '.css', '.vue'],
    alias: {
        'vue$': 'vue/dist/vue.esm.js'
    }
}

搭建本地服务器

webpack提供了一个可选的本地开发服务器,这个本地服务器基于node.js搭建,内部使用express框架,可以实现我们想要的让浏览器自动刷新显示我们修改后的结果.

在webpack中使用之前需要先安装它
npm install --save-dev webpack-dev-server@2.9.1

devserver是作为webpack中的一个选项,选项本身可以设置如下属性:

  • contentBase: 为哪一个文件夹提供本地服务,默认是根文件夹,我们这里要填写./dist
  • port: 端口号,默认8080
  • inline: 页面实时刷新
  • historyApiFallback: 在SPA页面中,依赖HTML5的history模式

webpack.config.js文件配置修改如下:

devServer: {
    contentBase: './dist',
    inline: true
}

配置scripts: --open参数表示直接打开浏览器

"dev": "webpack-dev-server --open"

配置文件的分离

目的: 针对不同环境配置不同的需求.
需要下载webpack-merge来合并不同配置npm install webpack-merge --save-dev

const webpackMerge = require('webpack-merge');
// 所有环境通用的配置
const default = require(./config/default.config)

module.exports = webpackMerge(default, {
    // 针对当前环境特有的配置
})

通过配置package.json脚本运行不同环境的配置

"script": {
    "build": "webpack",
    "dev": webpack-dev-server --open --config ./config/default.config.js,
    "prod": "webpack --config ./config/prod.config.js" 
}

runtime-compiler和runtime-only两种模式

区别:

  1. runtime-only比runtime-compiler轻。
  2. runtime-only运行更快
  3. runtime-only只能识别render函数,不能识别template。之所以可以在.Vue文件中写template是因为.Vue文件中的template先被vue-template-compiler翻译成了render函数。
  4. 两种模式生成的代码模板的区别只在main.js中:

Vue程序运行过程

步骤:

  1. 将Vue中的模板进行解析,将其解析成abstract syntax tree(ast)抽象语法树
  2. 将抽象语法树编译成render函数
  3. 将render函数再翻译成virtual dom(虚拟dom)
  4. 将虚拟dom转成真实UI显示在浏览器上

runtime-only更快的原因

runtime-only省略了Vue程序运行过程的第一步和第二步,所以体积更小,速度更快。

render函数的使用

render函数的由来

render: function(createElement) {
	return createElement(App);
}

// ES6 语法
render(createElement) {
	return createElement(App);
}

// 进一步缩写
render(h) {
	return h(App);
}

// 箭头函数
render: h => h(App);

h来自单词 hyperscript,这个单词通常用在virtual-dom的实现中。Hyperscript本身是指生成HTML结构的script脚本,因为HTML是hyper-text markup language的缩写(超文本标记语言)
h 函数的本质是createElement 函数,这个函数的作用就是生成一个 VNode节点,render 函数得到这个 VNode 节点之后,返回给 Vue.js 的 mount 函数,渲染成真实 DOM 节点,并挂载到根节点上

render函数的使用

const cpn = {
	template: '<div>apple</div>'
    data() {
    	return {}
    }
}
new Vue({
	el: '#app',
    render: createElement => {
    	// 用法1
        return createElement('标签', '相关数据对象(可省略)', ['内容数组']);
        // 基本使用
        return createElement('div', {class: 'box'}, ['apple']);
        // 嵌套使用
        return createElement('div', {class: 'box'}, ['apple', createElement('h2', ['标题'])]);
        
        // 用法2: 传入一个组件对象
        return createElement(cpn);
    }
})