前端工程化与「Webpack」

# 前端工程化

  近几年来,前端领域飞速发展,前端的工作早已不再是切几张图,写几个页面那么简单,项目比较大时,很可能会多人协同开发,模块化,组件化,CSS预编译等技术也被广泛的使用。前端自动化(半自动化)工程已经成为现在的主流。前端工程化主要解决一下问题

  • Javascript、CSS 代码的合并和压缩
  • CSS 预处理:Less,Sass, Stylus的编译
  • 生成雪碧图
  • ES6 转ES5 语法
  • 模块化
    ...

# Gulp 与 Webpack

  相信很多小伙伴都不仅知道gulp和webpack,还了解grunt,他们有什么区别呢?

  经过Gulp和Grunt合并压缩后的代码仍然是你写的代码,只是局部变量名被替换了,一些语法做了转换而已,整体的内容并没发生变化。

  而webpack打包后的代码已经不只是你写的代码,其中夹杂了很多webpack自身的模块处理代码。在编译过程中,webpack工程会自动载入一些内容。

 文章主旨限制这里介绍相对粗糙,对与他们的更详细的内容,感兴趣的朋友可以参见 Grunt、Gulp 与 Webpack 一文

# Webpack 与工程

  Webapck 主要适用场景是 单页面富应用(SPA),SPA通常是由一个htnl文件,和一堆按需加载的js组成。webpack的依赖关系图如下所示

8646214-3ecc4b4a987979cf.png

  如上图,左侧是业务中我们写的各种文件,包括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这个文件中。

  或许你会问:为什么实际项目中的html并没有这个main.js?那是因为实际项目中通常我们会在webapck的插件配置项中添加HtmlWebpackPlugin。又为什么要这个插件? 生产环境中常需要生成bundle_[hash].js的动态文件名来最大化且合理的利用缓存,而这个hash值一般是一个MD5 序列号,如果内容有更新就需要改变,因此无法固定bundle.js的文件名,而插件HtmlWebpackPlugin就是动态生成index.html并且提供正确的MD5号到bundle.js文件名上,因此,index.html会更新,而不是我们项目里写的那个index.html。

| ES6中的 exportimport

  在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.jswebpack.prod.conf.js, 通常还会有一个webpack.base.conf.js用来配置两个版本相同配置,然后通过importmerge形式添加到各自版本中

  对于脚本中的配置,除了 --open 很多 --config之外,还有几个常用的一起在这里总结

  • --open: 在执行命令是自动在浏览器打开页面
  • --config: 指明读取的配置文件的路径
  • --progress: 在控制台打印编译过程信息
  • --host: 指明执行的IP地址,默认是 127.0.0.1,也就是 localhost
  • --port: 指明执行时启用的端口号
  • --watch: 根据构建是得到的依赖关系,对项目所依赖的所有文件进行监听,发生改变即重新构建(该功能现在一般使用webpack-dev-server自动刷新机制和热替换HMR机制替代)
  • --hide-modules: 执行编译时不将webpack模块内容添加到编译输出文件中
    ......


到此我们就绪了一切准备工作,可以开始配置Webpack了

| 就是一个js文件而已

  关于webapck的配置,其实就是一个js文件:webpack.config.js 这里给出一个基础的配置案例(实际上往往我们会拆分成dev,prodbase三个配置文件),围绕案例我们来分析配置的几个要点:

'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_ENVproduction即生产模式,那么使用./config/index.jsbuild下的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,productionnone,可以启用对应环境下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代码文件如下:这里还mergeprod.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~

猜你喜欢

转载自blog.csdn.net/weixin_34236497/article/details/87229852

相关文章