# 前端工程化
近几年来,前端领域飞速发展,前端的工作早已不再是切几张图,写几个页面那么简单,项目比较大时,很可能会多人协同开发,模块化,组件化,CSS预编译等技术也被广泛的使用。前端自动化(半自动化)工程已经成为现在的主流。前端工程化主要解决一下问题
- Javascript、CSS 代码的合并和压缩
- CSS 预处理:Less,Sass, Stylus的编译
- 生成雪碧图
- ES6 转ES5 语法
- 模块化
...
# Gulp 与 Webpack
小伙伴们应该都或多或少听说过Gulp和webpack,他们有什么区别呢?
Gulp与Webpack都是前端打包工具,不过Gulp是流管理,合并压缩后的代码仍然是你写的代码,只是局部变量名被替换了,一些语法做了转换而已,整体的内容并没发生变化。
而webpack打包后的代码已经不只是你写的代码,其中夹杂了很多webpack自身的模块处理代码。在编译过程中,webpack工程会自动载入一些内容。
# Webpack 与工程
Webapck 主要适用场景是 单页面富应用(SPA),SPA通常是由一个htnl文件,和一堆按需加载的js组成。webpack的依赖关系图如下所示
如上图,左侧是业务中我们写的各种文件,包括js
,less
,jpg
,这些格式的文件通过特定的加载器(Loader)编译之后,最终统一生成为右侧这些静态资源。
在Webpack的世界里,一张图片,一个CSS甚至是一个字体,都被称为是一个模块,彼此存在依赖关系,webpack就是用来处理模块间的依赖关系,并把他们进行打包
在传统的html中,如果我们需要引入一个css文件,通常在html页面的<head>
部分使用<link>
将其引入
<link rel="stylesheet" href="/css/index.css">
而在webpack中,我们不需要在html中添加,而是直接在.js
文件中使用。打包时,index.css
会被打包进一个js
文件里,通过动态创建<style>
的形式来加载css样式,当然也可以进一步配置,在打包编译时把所有的css都提取出来,生成给一个css文件
import 'src/css/index.css' // ES 6
require('src/css/index.css')
| SPA 的一个html文件
SPA是由一个html文件和一堆按需加载的js组成,而这个html结构可能会非常简单
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>webpack app</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="/dist/main.js"><script>
</body>
</html>
这就是整个SPA的html内容,理论上他可以实现譬如淘宝,知乎这样的大型项目,而其他的内容,都集中在了神奇的main.js
这个文件中
| ES6中的 export
与 import
在SPA项目里,一个模块就是一个JS文件,它拥有独立的作用域,为了使外部能读取里面定义的变量,需要将一个配置文件作为模块导出export
。这个配置文件可以是对象、数组,常量、函数等。譬如有 config.js
文件如下
// 普通变量
export var config = {
version: '2.0'
}
// 函数
export function add(a, b) {
return a + b
}
这些文件被导出之后,在需要使用的模块使用import
导入,就可以在这个文件内使用这些模块了。譬如有main.js
文件如下
import { _, config, add } from './config.js'
console.log(config) // { version: '2.0' }
console.log(add(1, 2)) // 3
在导入中写到的模块名称都是在export文件中设置的,也就是说,用户必须要提前知道模块中的名称有些什么叫什么才能将其导出。有时候我们不想去了解名称叫什么,或者需要自定义名称,我们可以在模块导出时可以使用export default
来实现
// 也可以使用默认导出
export default {
version: '2.0'
}
导入时就可以使用自定义的名称了
import conf from './config.js'
我们还经常需要安装一些第三方库,在webpack中也可以直接导入。
import Vue from ’vue‘
import Vuex from 'vuex'
import $ from 'jquery'
# Webpack 的配置
执行npm init
并按回车跳过一些选项,可以得到一个package.json文件,
使用NPM本地局部安装webpack: npm install webpack --save-dev
接着安装 npm install webpack-dev-server --save-dev
,安装完成后,可以看到在package.json文件中也多了这两个配置信息及版本号。
| 添加启动脚本
在 npm 配置文件 package.json 文件的script
中添加一个快速启动 webpack-dev-server服务的脚本
{
"scripts": {
"test": "echo \"Error: on test specified\" && exit 1 ",
"dev": "webpack-dev-server --open --config build/webpack.config.js"
}
}
执行时运行 npm run dev
命令,就会执行脚本中dev
字段的命令,其中,--open
指的是运行命令后,自动打开浏览器,--config
指读取配置文件的路径。webpack的项目中,webpack配置通常都命名为webpack.config.js
, 并存放在build
文件夹下。
当然如果区分开发版本和正式版本,常区分命名webpack.dev.conf.js
和 webpack.prod.conf.js
, 通常还会有一个webpack.base.conf.js
用来配置两个版本相同配置,然后通过import
和merge
形式添加到各自版本中
对于脚本中的配置,除了 --open
很多 --config
之外,还有几个常用的一起在这里总结
-
--open
: 在执行命令是自动在浏览器打开页面 -
--config
: 指明读取的配置文件的路径 -
--progress
: 在控制台打印编译过程信息 -
--host
: 指明执行的IP地址,默认是 127.0.0.1,也就是localhost
-
--port
: 指明执行时启用的端口号 -
--hide-modules
: 执行编译时不将webpack模块内容添加到编译输出文件中
......
到此我们就绪了一切准备工作,可以开始配置Webpack了
| 就是一个js文件而已
关于webapck的配置,其实就是一个js文件:webpack.config.js
这里给出一个基础的配置案例(实际上往往我们会拆分成dev
,prod
和base
三个配置文件),围绕案例我们来分析配置的几个要点:
'use strict'
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const merge = require('webpack-merge')
const utils = require('./utils')
var config = {
// 入口
entry: {
app: './src/main.js'
},
// 出口
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'), // @符号代表路径‘~/src’
'@root': path.resolve(__dirname, './')
}
},
// 加载器配置(需要加载器转化的模块类型)
module: {
rules: [
{
test: '/\.css$/',
use: [ 'style-loader', 'css-loader' ]
}
]
}
// 插件
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
]
}
module.exports = config
1. 入口( Entry
)
entry
的作用是告诉webpack 从哪里开始寻找依赖,并且编译。通常情况下,入口文件都是src
文件夹下的main.js
文件。也就是说webpack会从main.js
开始工作。
2. 出口( Output
)
Output
是用来配置编译后的文件存储的路径和文件名。path
用来存放打包后文件的输出目录,是必填项;filename
用于指定编译后输出文件的名称;publicPath
指定资源文件引用的目录,如你资源在CDN上,这里可以填CDN路径。这里需要提一下,案例中使用的代码片段解释如下。具体信息见第5点‘模式’
如果当前‘模式’
process.env.NODE_ENV
是production
即生产模式,那么使用./config/index.js
下build
下的assetsPublicPath
配置,否者使用dev
下的配置。
3. 加载器(Loaders
)- webpack最重要的功能
作为开箱即用的自带特性,webpack自身只支持javascript。而loader能够让webpack处理那些非javascript的文件, 并将他们转化成有效的模块,然后添加到模块图中供给程序使用。
之前说过,webpack里每一个文件都是一个模块,不同后缀名的模块需要不同的加载器来处理。加载器并不是webapck本身就有的内容,需要使用什么加载器需要用户用使用npm来安装,如:
npm install css-loader --save-dev
npm install style-loader --save-dev
// style-loader用作于热更新功能
在 webpack 配置中定义 loader 时,要定义在 module.rules 中,而不是 rules。为了使你受益于此,如果没有按照正确方式去做,webpack 会给出警告。
在module
对象的rule
属性中可以指定一系列的loader
加载器,每个加载器都必须配置两个特性
- test 属性: 标识出应该被对应loader转换的是哪个文件或那种类型文件
- use 属性: 表示转换是应该使用哪个loader,它的值可以是数组或字符串,如果是数组,它的编译顺序是从后往前
案例中所表达的意思是:嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.css' 的路径」时,在你对它打包之前,先使用
css-loader
转换一下,在使用style-loader
转换一下再打包。
loader有三种使用方式
(1)配置(建议): 在 webpack.config.js 文件中指定 module
选项rules
属性中指定loader
。
(2)内联:在每个 import 语句中显式指定 loader。
import Styles from 'style-loader!css-loader?modules!./styles.css';
(3)CLI。在 shell 命令中指定它们。
webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
以一个vue项目为例,通常需要的Loader
列表如下,其中涉及了vue文件的解析,ES6转ES5语法,图片资源,视频资源,文本字体等各种类型模块的处理
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
代码中频繁出现的utils.assetsPath
的定义如下
//默认返回的是~/dist/static/_path
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
4. 插件(plugins
)
loader 被用于转换某些类型的模块,而插件则用于执行范围更广的任务,包括:打包优化,资源管理和注入环境变量等等。
使用插件前我们需要先用npm
安装,然后用require
将其引入,再把他添加到plugins
数组中
// 为你的应用程序生成一个html,然后自动注入生成的bundle
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 用于访问内置插件,如热更新
const webpack = require('webpack')
// 用于合并其他配置文件到本文件中
const merge = require('webpack-merge')
webpack的内置插件用法简单直接,案例中我们使用了它的热更新插件
new webpack.HotModuleReplacementPlugin()
除此之外,还有许多请参见webpack插件列表
5. 模式 (mode
)
通过将模式设置为 development
,production
或 none
,可以启用对应环境下webpack内置的优化。默认production
.
在案例中,plugins
选项提到了一个DefinedPlugin
,代码片段为
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
})
其作用就是 设置开发环境变量为dev.env.js
中配置的NODE_ENV
,正因为采用了这种方案,在Output
选项的publicPath
属性设置时我们才能使用process.env.NODE_ENV
软编码识别当前环境变量。
DefinePlugin 是webpack的一个接口,它允许我们创建一个在编译时可以配置的全局变量,这对我们区分开发模式和发布模式的构建允许不同的行为非常有用,比如,在开发模式中记录日志,而在发布模式中不记录日志。本例子中我们只是使用其区分了环境变量
dev.env.js
代码文件如下:这里还merge
了prod.env.js
文件里的内容。经过配置后的模式(mode
)的信息,其实就是这两个配置文件中的NODE_ENV
变量
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
API_ROOT: '"http://localhost:3000"'
})
# Webpack的模块热替换 HMR
为避免篇幅过长阅读疲劳,该模块内容请参见Webpack如何实现热发布一文
结束语
前端的大肆流行绝非偶然,正是这些工程化的思想引入,使得前端向大前端迈进成为可能,更加丰富的软编码思路,也使得前端适应性更强,灵活度更大,自动化(半自动化)的实现使得编码更加便捷,效率更高。做好前端,不能只做页面dog~
网友评论