Skip to content

Latest commit

 

History

History
450 lines (381 loc) · 18.1 KB

FAQ.md

File metadata and controls

450 lines (381 loc) · 18.1 KB

常见问题答疑

为什么做这个工程化实践?

思考下 Web 网站一般包含些什么东西?

  • 多个 html 页面(例如首页/关于页/产品介绍页等独立页面)
  • 每个 html 页面包含
    • 一个或多个第三方库(js/css/res)
    • 一个或多个该网站公共的样式和资源(css/res)
    • 一个或多个该网站公共的逻辑(js)
    • 一个或多个该页面专有的样式和资源(css/res)
    • 一个或多个该页面专有的逻辑(js)

正式发布时又会是什么样子?

  • 发布的目录结构与开发时最好保持一致
  • 第三方库合并/压缩成一个文件或者直接引用公共 CDN
    • vendor.js
    • vendor.css
  • 合并/压缩该网站公共的样式
    • app.css
  • 合并/压缩该网站公共的逻辑
    • app.js
  • 合并/压缩每个页面专有的样式
    • page.css
  • 合并/压缩每个页面专有的逻辑
    • page.js
  • 为了提升前端性能, 以上资源最好做基于文件 hash 的强缓存

因此我们理想的发布后的 Web 网站应该是这样的, 请参考网站项目目录结构规范

网站/
├── lib/
|   |── app/
|   |   |── app.css
|   |   |── app.js
|   |   └── res/
|   |── vendor/
|   |   |── vendor.css
|   |   |── vendor.js
|   |   └── ...
|   └── cdn/
|
├── page1/
|   |── page1.html
|   |── page1.css
|   |── page1.js
|   └── res/
|       └── page1.jpg
|
└── page.../

我们的 page1.html 应该差不多是这个样子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>page1</title>
    <link rel="stylesheet" href="../lib/vendor/vendor.css">
    <link rel="stylesheet" href="../lib/app/app.css">
    <link rel="stylesheet" href="page1.css">
</head>
<body>
    <p>page1 的内容</p>
    <script src="../lib/vendor/vendor.js"></script>
    <script src="../lib/app/app.js"></script>
    <script src="page1.js"></script>
</body>
</html>

或者我们使用公共 CDN 来引入第三方依赖的库

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>page1</title>
    <link rel="stylesheet" href="http://freecdn.com/vendor1.css">
    <link rel="stylesheet" href="http://freecdn.com/vendor2.css">
    <link rel="stylesheet" href="../lib/app/app.css">
    <link rel="stylesheet" href="page1.css">
</head>
<body>
    <p>page1 的内容</p>
    <script src="http://freecdn.com/vendor1.js"></script>
    <script src="http://freecdn.com/vendor2.js"></script>
    <script src="../lib/app/app.js"></script>
    <script src="page1.js"></script>
</body>
</html>

理想很丰满现实很骨干, 想要达到理想中开发的效果, 我们首先需要应对各种各样的依赖问题, 然后就是资源文件做 hash 强缓存后涉及的路径替换问题

  • JS 依赖
    • CommonJS 模块(托管在 npm 或本地的)
    • AMD 模块
    • UMD 模块
    • 全局模块(那些直接暴露在 window 的全局变量)
    • 插件类模块(例如 jQuery 插件), 只是增强功能, 不会暴露出全局变量
  • CSS 依赖
  • 资源依赖

在实践了多种前端工程化方案后, 最终选择通过 webpack 来达到我的上述目标

  • 支持多页面, 方便扩展
  • 支持多环境模式
  • 面向未来的开发模式(例如使用 ES2015)
  • 能打包各种模块(终于不用担心全局变量了)
  • Code Splitting 实现按需加载
  • 方便抽离出公共部分(不再是简单的合并成一个 all in one 的 JS 文件)
  • 透明的做基于文件 hash 的强缓存, 绝对不需要你手工去修改资源的路径

如何添加其他的页面?

约定入口模块的 JS/HTML 文件和页面同名, 例如你现在想添加一个 page1 页面

那么应该 src 目录应该为

src/
└── page1/
    |── page1.html -- 页面模版
    |── page1.js   -- 页面入口模块
    |── page1.css
    └── res
// webpack.config.js
// 增加页面入口模块(去掉页面入口模块的 JS 文件后缀即可)
addPageEntry('page1/page1');
// 这样就相当于为 webpack 添加了入口模块为 'page1/page1': 'page1/page1.js'

// 如果是某个页面的子模块, 例如 page1/page1/subpage
addPageEntry('page1/page1/subpage');
// 这样就相当于为 webpack 添加了入口模块为 'page1/page1/subpage': 'page1/page1/subpage.js'

如何处理通过 Code Spliting 懒加载的模块中包含的 CSS?

  • 方式一: 给 ExtractTextPlugin.extract 配置 notExtractLoader, 同时配置 ExtractTextPlugin allChunksfalse (默认值)

    例如:

    ExtractTextPlugin.extract('style-loader', 'css')
    new ExtractTextPlugin('style.css', {allChunks: false})

    由于 new ExtractTextPlugin 的默认配置只提取入口模块中的 CSS, 对于通过 Code Spliting 加载的模块, 其中包含的 CSS 内容会作为文本待在模块中(不会被提取成单独的 CSS 文件), 因此最终没有在页面中生效(因为此时没有使用 style-loader).

    这就是为什么我们需要给 ExtractTextPlugin.extract 配置 notExtractLoader, 它的作用就是当模块中的 CSS 没有被提取出来时, 告诉 webpack 应该使用另外的什么 loader 来加载这个文件, 这里我们当然就是使用 style-loader 来动态的在页面中插入 style 元素来加载异步模块中的样式了

  • 方式二: 配置 ExtractTextPlugin allChunkstrue, 提取所有模块中的 CSS, 包括 Code Spliting 分离出来的模块

    这样通过 Code Spliting 加载的模块中的 CSS 会包含在入口模块的 CSS 中(例如 index.css). 相当于只异步加载 JS 模块, JS 模块中包含的样式合并到入口模块的 CSS 中一开始就加载了.

如何指定的开发模式?

通过环境变量(MODE)来控制, 例如在 scripts 中设置 cross-env MODE=dev, 你可以扩展出其他模式, 例如预发布模式

最终构建后的效果是怎样的?

开发模式下

开发模式下的模块

非开发模式下

dist/
├── index/                          -- 页面模块 
|   |── index-8ef142e.css           -- 页面 CSS
|   └── index-9f9aa74.js            -- 页面 JS
|
├── about/
|   |── about.html
|   |── about-5f5d601.css
|   |── about-0ec759d.js
|   └── foo
|       |── foo.html
|       |── foo-5f5d601.css
|       └── foo-c14261c.js
|
├── lib/                            -- 公共模块 
|   |── vendor-7595e55.js           -- 第三方 JS
|   |── app-ebb4047.js              -- 公共 JS
|   └── 3-181fa6c.js                -- 异步模块
|
├── res/                            -- 所有依赖的静态资源
|   |── what-is-webpack-0fa90f9.png
|   └── ...
|
├── vendor-6c72d88.css              -- 第三方 CSS
├── app-5f5d601.css                 -- 公共 CSS
└── index.html                      -- 首页

如何扩展为支持 Vue 的项目?

  • 安装 Vue (包括 vue-loader), 参考vuejs-templates/webpack-simple

    npm install vue --save
    npm install vue-loader vue-template-compiler --save-dev
    
  • 添加 vue-loader 来处理(.vue)单文件组件

    当然了, 如果你不需要单文件组件功能, 可以不用配置和安装 vue-loadervue-template-compiler

    // config/webpack.base.config.js
    // module.loaders
    {
        test: /\.vue$/,
        loader: 'vue-loader'
    }
  • 配置 vue-loader 参数, 增加对 <script>es2015 的支持

    当然了, 如果你不需要在 .vue 文件中使用 es2015, 可以不用这么配置, 就只能使用 CommonJS 的模块方式, 但这种情况基本上不存在.

    不配置就在 .vue 文件中使用 es2015 的话会出现这样的错误: Uncaught SyntaxError: Unexpected token export

    // config/webpack.base.config.js
    //
    // https://vue-loader.vuejs.org/en/options.html
    vue: { // webpack1 的配置方式
        loaders: {
            // http://vue-loader.vuejs.org/en/configurations/extract-css.html
            // 默认使用 .vue 单文件组件时(不配置下面的这个 css 项), 不管你是以同步 import 的方式来使用这个组件,
            // 还是以异步的 code spliting 方式来使用这个组件, 组件中的 css 都是通过 style 元素添加到页面中来的, 而非合并到一个 css 文件
            //
            // 因此需要配置 ExtractTextPlugin 提取出组件模块中的 css 内容, 最终合并会一个 css 文件.
            //
            // 对于通过 code spliting 异步加载的 .vue 组件, css 处理的逻辑与[如何处理通过 Code Spliting 懒加载的模块中包含的 CSS?](https://github.com/appbone/webpack-driven-web/blob/master/FAQ.md#如何处理通过-code-spliting-懒加载的模块中包含的-css)是一样的
            // 对于 import 方式同步加载使用的组件, css 内容会合并到入口模块的 css 文件中,
            // 对于 code spliting 方式异步加载使用的组件, css 内容由于没有从模块中提取出来, 而降级到以 style 元素添加到页面上
            // 
            // 这里给出通过 code spliting 方式懒加载 .vue 组件的示例
            // new Vue({
            //     el: '#root',
            //     components: {
            //         'app': function(resolve, reject) {
            //             setTimeout(function() {
            //                 require.ensure(['./app.vue'], function() {
            //                     var App = require('./app.vue');
            //                     console.log('lazyMod .vue', App);
            //                     resolve(App);
            //                 });
            //             }, 3000)
            //         }
            //     }
            // });
            css: ExtractTextPlugin.extract('style-loader', 'css?' + JSON.stringify(config.cssLoader)),
            js: 'babel-loader?{"presets":["es2015"]}'
        }
    }
  • 通过 alias 配置使用 vue.js 的哪个发行版

    // config/webpack.base.config.js
    // resolve.alias
    //
    // Explanation of Build Files
    // https://github.com/vuejs/vue/tree/dev/dist#explanation-of-build-files
    //
    // vue 模块默认使用的是 Runtime-only 版
    // "main": "dist/vue.runtime.common.js",
    //
    // 如果你不需要编译器, 在代码中不使用模版(template)功能, 则可以使用 runtime 版
    // 例如: render: h => h(App)
    //
    // 如果你搞不清使用了什么版本, 或者到底要使用什么版本, 出现了下面的错误,
    // 那么你可以使用 UMD 版的 'vue/dist/vue.[min].js' 或者 CommonJS 版的 vue.common.js
    // * Failed to mount component: template or render function not defined. 
    // * You are using the runtime-only build of Vue where the template option is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
    // 
    // 具体情况可以参考: 独立构建和运行构建, 它们的区别在于前者包含模板编译器而后者不包含
    // * 模板编译用于编译 Vue 模板字符串成纯 JavaScript 渲染函数。如果你想用 template 选项, 你需要编译
    // * 模板编译器的职责是将模板字符串编译为纯 JavaScript 的渲染函数
    // * 独立构建包含模板编译器并支持 template 选项。 它也依赖于浏览器的接口的存在,所以你不能使用它来为服务器端渲染
    // * 运行时构建不包含模板编译器,因此不支持 template 选项,只能用 render 选项
    //   * 但即使使用运行时构建,在单文件组件中也依然可以写模板,因为单文件组件的模板会在构建时预编译为 render 函数
    //   * 运行时构建比独立构建要轻量
    // https://cn.vuejs.org/v2/guide/installation.html#独立构建-vs-运行时构建
    'vue$': 'vue/dist/vue.js'
  • 修改 vendor 添加 vue

    // config/project-config.js
    vendor: [
        '...',
        'vue'
    ]
  • 然后愉快的写 Vue

    <template>
        <div class="example">{{ msg }}</div>
    </template>
    
    <script>
    // https://vue-loader.vuejs.org/en/start/spec.html#src-imports
    // 很多人可能没有了解到, 其实 .vue 文件的各个部分也是可以通过 src import 进来的
    export default {
        data() {
            return {
                msg: 'Hello world!'
            }
        }
    };
    </script>
    
    <style>
    .example {
        color: red;
    }
    </style>
    import Vue from 'vue';
    import App from './App.vue';
    
    new Vue({
        el: '#root',
        components: {
            'app': App
        }
    });
    <div id="root"><app></app></div>

如何扩展为支持 React 的项目?

  • 安装 React (包括 babel preset), 参考Babel JSX

    npm install react react-dom --save
    npm install babel-preset-react --save-dev
    
  • 修改 babel-loader 添加 react

    // config/webpack.base.config.js
    // module.loaders > babel-loader
    presets: ['es2015', 'react']
  • 修改 vendor 添加 react

    // config/project-config.js
    vendor: [
        '...',
        'react',
        'react-dom'
    ]
  • 然后愉快的写 React

    import React, {
        Component
    } from 'react';
    import ReactDOM from 'react-dom';
    
    class App extends Component {
      constructor(props) {
        super(props);
      }
    
      render() {
        return (
          <div className="test">
            {this.props.a}
          </div>
        );
      }
    }
    
    ReactDOM.render(<App a="可以用 react 了" />, document.getElementById('root'));

什么时候支持 webpack 2.x?

再等等吧, 生态还不成熟... 况且 webpack 1.x 已经解决了我的问题, 有兴趣的可以基于这个项目自己做一个 webpack 2.x 的版本

参考