模块化开发
简介
问题的产生: 功能越复杂,代码量也随之增加,通常会将代码组织在多个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是一个局部变量。
解决方法: 我们可以使用将需要暴露到外面的变量,使用一个模块作为出口。
步骤:
- 在匿名函数内部定义一个对象。
- 给对象添加各种需要暴露到外面的属性和方法(不需暴露的直接定义即可)。
- 将对象返回,并在外面使用一个ModuleA接受。
- 在其他文件中使用属于该模块的属性和方法。
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两种模式
区别:
- runtime-only比runtime-compiler轻。
- runtime-only运行更快
- runtime-only只能识别render函数,不能识别template。之所以可以在.Vue文件中写template是因为.Vue文件中的template先被vue-template-compiler翻译成了render函数。
- 两种模式生成的代码模板的区别只在main.js中:
Vue程序运行过程
步骤:
- 将Vue中的模板进行解析,将其解析成abstract syntax tree(ast)抽象语法树
- 将抽象语法树编译成render函数
- 将render函数再翻译成virtual dom(虚拟dom)
- 将虚拟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);
}
})