function incallback() {
  setTimeout(function () {
    alert('Done is better than perfect.  -  via Facebook')
    document.getElementById('incallback').innerHTML = 'Welcome to my session!'
  }, 300)
}

前端模块化小史,Webpack 入门

by Jimmy Lv

模块化管理?

JavaScript 模块化

  • ES6 Module
  • CommonJS
  • AMD
  • UMD

现在即未来:ES6 模块规范

// profile.js
var firstName = 'Michael'
var lastName = 'Jackson'

var sayHi = () => {
  console.info('I am ' + firstName + ',' + lastName + '!')
  console.info(`I am ${firstName}, ${lastName}!`)
}

export { firstName, lastName, sayHi }
// main.js
import { firstName, lastName, sayHi } from './profile'

import * as profile from './profile'
profile.sayHi()

民间两大规范

  • CommonJS: 同步加载,主要用于 NodeJS 服务器端;
  • AMD: 异步加载,通过 RequireJS 等工具适用于浏览器端。

过去式:CommonJS 规范

NodeJS: JavaScript 要逆袭!我是窜天 🐵,我要上天!


var firstModule = require('firstModule')

//playing code...

module.export = anotherModule

JavaScript 组件发布平台:NPM


🐒🐒🐒🐒🐒:前端项目要是能在浏览器中更方便地使用 NPM 资源就好了!

过去式:AMD 规范

即 (Asynchronous Module Definition)

define(['firstModule'], function (module) {
  //playing code...

  return anotherModule
})

Browserify.js

🐒🐒🐒🐒🐒:要是能在浏览器使用 require 同步语法加载 NPM 模块就好了!


var firstModule = require('firstModule')

//playing code...

module.export = anotherModule
define(['firstModule'], function (module) {
  //playing code...

  return anotherModule
})

「通用」模式:UMD

即 (Universal Module Definition)

;(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['jquery'], factory)
  } else if (typeof exports === 'object') {
    // Node, CommonJS
    module.exports = factory(require('jquery'))
  } else {
    // 浏览器全局变量(root 即 window)
    root.returnExports = factory(root.jQuery)
  }
})(this, function ($) {
  //    方法
  function myFunc() {}
  //    暴露公共方法
  return myFunc
})

前端工程化需求

前端模块化框架肩负着 模块管理、资源加载 两项重要的功能,这两项功能与工具、性能、业务、部署等工程环节都有着非常紧密的联系。因此,模块化框架的设计应该最高优先级考虑工程需要。


  • 依赖管理
  • 按需加载
  • 请求合并

新思路:「不」在运行时分析依赖。

  • 借助构建工具来做线下分析:


利用构建工具在线下进行模块依赖分析,然后把依赖关系数据写入到构建结果中,并调用模块化框架的依赖关系声明接口,实现模块管理、请求合并以及按需加载等功能。

主角:Webpack

「任何静态资源都可以视作模块,然后模块之间还可以相互依赖。」

特性

  1. 兼容 CommonJS 、 AMD 、ES6 语法
  2. 支持打包 JS、CSS、图片等资源文件
  3. 串联式 loader 以及插件机制
  4. 独立配置文件 webpack.config.js
  5. 代码切割 chunk,实现按需加载
  6. 支持 SourceUrls 和 SourceMaps
  7. 具有强大的 Plugin 接口,使用灵活
  8. 支持异步 IO 并具有多级缓存,增量编译速度快

一个简单的 React 例子

// hello.js
import React, { Component } from 'react'

class Hello extends Component {
  render() {
    return <div>Hello, {this.props.name}!</div>
  }
}

export default Hello
// entry.js
import React from 'react'
import Hello from './hello'

React.render(<Hello name="Jimmy" />, document.body)

Webpack 配置文件

// webpack.config.js
var path = require('path')

module.exports = {
  entry: path.resolve(__dirname, './src/entry.js'),
  output: {
    path: path.resolve(__dirname, './assets'),
    filename: 'bundle.js',
  },

  module: {
    loaders: [{ test: /\.js?$/, loaders: 'babel-loader', exclude: /node_modules/ }],
  },

  resolve: {
    extensions: ['', '.js', '.json'],
  },
}

打包完毕

// index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>React Sample</title>
</head>
<body>
<script src="./assets/bundle.js"></script>
</body>
</html>

实战:重构现有项目

  • 代码热加载
  • 配置 NPM Script
  • babel-loader: 预编译 ES6
  • ng-annotate-loader: 确保依赖安全注入
  • less-loader: 使用 Less 替换 CSS
  • ngtemplate-loader: String 化 HTML 模板

代码热加载:webpack-dev-server

webpack-dev-server 是一个基于 Express.js 框架的静态资源 Web 服务器。开发服务器会监听每一个文件的变化,进行实时打包,并推送通知给前端页面,从而实现自动刷新。


module.exports = {
    entry: {
        app: ['webpack/hot/dev-server', './app.js']
    },
    output: {
        path: './assets',
        filename: 'bundle.js'
    },
    ...
}

默认端口 8080:localhost:8080/webpack-dev-server/

配置 NPM Script

"scripts": {
    "dev": "webpack -w --bail --display-error-details",
    "start": "webpack-dev-server --history-api-fallback --hot --inline --progress",
    "build": "webpack -p"
}
npm run dev # 提供 watch 方法,实时进行打包更新并打印出错信息
npm start # 启动服务器,浏览器直接访问的是 index.html
npm run build # 输出 production 环境下的压缩打包代码

babel-loader 预编译 ES6

module: {
    loaders: [
      {test: /\.js$/, exclude: /node_modules/,
        loader: 'ng-annotate!babel?presets=es2015'}
    ]
}
require('./style/base.less')

import angular from 'angular'
import ngRoute from 'angular-route'

import githubService from './app/services/githubService'
import MainCtrl from './app/controllers/mainController'
import Components from './app/components/components.module'

ng-annotate-loader 依赖安全注入

controller($http, $routeParams, base64) {
    'ngInject';

    var vm = this;

    vm.$onInit = () => {
        ...
    }
}
controller:["$http","$routeParams","base64",function(e,t,n){"ngInject" ...}]

使用 Less 替换 CSS

{test: /\.less$/, loader: "style!css!less"},

{test: /\.(eot|woff|woff2|ttf|svg)(\?\S*)?$/,
    loader: 'url?limit=100000&name=./fonts/[name].[ext]'},
{test: /\.(png|jpe?g|gif)$/,
    loader: 'url-loader?limit=8192&name=./images/[hash].[ext]'}
import '../../node_modules/font-awesome/css/font-awesome.css'
import '../../assets/styles/bootstrap.css'
import '../../assets/styles/yue.css'
import '../../assets/styles/base.less'

String 化 HTML 模板

{test: /\.html$/, loader: 'ngtemplate!html?attrs[]=img:src img:ng-src'}
import './post.less'

export default {
  templateUrl: require('./post.html'),
  bindings: {
    pageContent: '<',
    showToc: '<'
  }
  controller() {
    ...
  }
import post from './post/post'

export default angular.module('app.note', []).component('post', post)

AngularJS 组件结构

app/
├── app.js
├── commons
│   ├── commons.module.js
│   ├── footer
│   │   ├── footer.html
│   │   └── footer.js
│   ├── header
│   │   ├── header.html
│   │   ├── header.js
│   │   └── header.less
├── configs
│   ├── app.config.js
│   ├── app.routes.js
│   ├── app.run.js
│   └── configs.module.js
├── features
│   ├── features.module.js
│   ├── apps
│   │   ├── apps.html
│   │   └── apps.js
│   └── note
│       ├── note.html
│       ├── note.js
│       ├── note.less
│       ├── note.module.js
│       ├── page
│       │   ├── page.html
│       │   ├── page.js
│       │   └── page.less
│       ├── post
│       │   ├── post.html
│       │   ├── post.js
│       │   └── post.less
└── services
    ├── githubService.js
    ├── musicService.js
    └── services.module.js

「越痛苦的事情越要早做」

var path = require('path')
var webpack = require('webpack')

module.exports = {
  context: __dirname,
  entry: {
    app: ['webpack/hot/dev-server', './app/app.js'],
  },
  output: {
    path: './dist',
    filename: 'bundle.js',
  },

  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'ng-annotate?add=true!babel-loader',
      },
      { test: /\.css$/, loader: 'style!css' },
      { test: /\.less$/, loader: 'style!css!less' },
      {
        test: /\.(eot|woff|woff2|ttf|svg)(\?\S*)?$/,
        loader: 'url?limit=100000&name=./fonts/[name].[ext]',
      },
      {
        test: /\.(png|jpe?g|gif)$/,
        loader: 'url-loader?limit=8192&name=./images/[hash].[ext]',
      },
      { test: /\.html$/, loader: 'ngtemplate!html?attrs[]=img:src img:ng-src' },
    ],
    noParse: [],
  },

  plugins: [new webpack.HotModuleReplacementPlugin()],

  resolve: {
    extensions: ['', '.js', '.json'],
    alias: {
      react: './pages/build/react',
    },
    modulesDirectories: ['node_modules', 'bower_components'],
  },
}

NoBackend Website