Skip to content

基于 webpack 的多页应用框架优化 #6

@jinlong

Description

@jinlong

mobile-intl 框架起源

基于多页框架项目 vue-multiple-pages 优化

基础 webpack 配置

配置文件

  1. 通常情况,根目录下面的 webpack.config.js 文件作为默认配置文件

  2. 自定义配置文件,使用 --config 指定配置文件

package.json

"scripts": {
  "build": "webpack --config prod.config.js"
}

常用配置项:

const path = require("path");

module.exports = {
  // 选择模式,webpack 可以根据选择启用内置优化
  mode: "production", // "production" | "development" | "none"

  // webpack 打包入口,单页应用配字符串,多页应用配对象或数组
  entry: "./app/entry", // string | object | array
  entry: {
    a: "./app/entry-a",
    b: ["./app/entry-b1", "./app/entry-b2"]
  },

  // 打包文件输出配置
  output: {
    // 所有文件的输出目录,必须是个绝对路径
    path: path.resolve(__dirname, "dist"), // string
    filename: "bundle.js", // string
    filename: "[name].js", // 多入口
    // 分块(chunks)入口的名字
    publicPath: "/mobile-intl/" // string
    // 输出路径相对于 HTML 页面的 url
  },

  // 处理各种模块的 Loader
  module: {
    rules: [{
        test: /\.jsx?$/,
        include: [path.resolve(__dirname, "app")],
        exclude: [path.resolve(__dirname, "app/demo-files")],
        loader: "babel-loader", // Rule.use: [ { loader } ] 的简写,loader 名称
        options: {
          // Rule.use: [ { options } ] 的简写,loader 配置
          presets: ["es2015"]
        }
      },
      {
        test: /\.html$/,
        use: [
          // 配置多个 loader
          "htmllint-loader",
          {
            loader: "html-loader",
            options: {
              /* ... */
            }
          }
        ]
      }
    ]
  },

  // 模块解析配置
  resolve: {
    // 模块搜索目录
    modules: ["node_modules", path.resolve(__dirname, "app")],
    // 文件扩展名
    extensions: [".js", ".json", ".jsx", ".css"],
    // 别名
    alias: {
      vue: "vue/dist/vue.js",
      vuex: "vuex/dist/vuex.js",
      "@": path.resolve("src")
    }
  },

  // 性能报告
  performance: {
    hints: "warning", // enum
  },
  // source map 如何生成,调试便利和编译速度如何权衡
  devtool: "source-map",
  // 配置 webpack-serve
  serve: {
    port: 1337,
    content: "./dist"
  },
  // 精确控制命令行输出日志
  stats: "errors-only",

  // webpack-dev-server 配置
  devServer: {
    proxy: {
      // 后端接口代理,可用于 mock 数据
      "/api": "http://localhost:3000"
    },
    contentBase: path.join(__dirname, "public"), // boolean | string | array, 静态文件目录
    compress: true, // 启用 gzip 压缩
    hot: true, // 模块热替换
    noInfo: true, // 热更新时仅输出错误和警告日志
    stats: { // 命令行日志信息
    },
  },

  // 插件配置
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin(),
  ]
};

全部配置见官方文档

快速生成自定义配置文件,可以用官方推荐工具

打包输出目录优化

精简多余的目录

// CSS 目录
const extractCSS = new ExtractTextPlugin({
  // filename: 'assets/css/[name].css',
  filename:  (getPath) => {
    return getPath('assets/css/[name].css').replace('views/', '');
  }
})

// HTML 目录
filename = filename.replace('views/', '')
const htmlConf = {
  filename: filename,
  template: path.replace(/.js/g, '.html'),
  inject: 'body',
  hash: true,
  chunks: ['commons', chunk]
}

// JS 目录
const _chunk = path.split('./src/pages/')[1].split('/app.js')[0]
const chunk = _chunk.replace('views/', '')

项目启动优化

From

npm start

To

# 自定义调试项目,customer 为项目的目录名字
npm start -- --index=customer

实现思路

想办法把 openPage 字段变成动态值

devServer: {
  open: true,
  openPage: 'mobile-intl/index.html',
}

如何实现

  1. 传入参数
npm start -- --index=customer
// package.json
scripts: {
  "start": "node ./tools/runDev.js",
}
  1. Node.js 截取参数
let params = process.argv
  1. 补全路径
let indexPage = params.find(function(it) {
  return it.indexOf('--index=') > -1  // 找到起始页标识
})
...
let indexPath = indexPage.split('--index=')[1]
indexPage = 'mobile-intl/'+ indexPath +'/index.html'
  1. 设置 webpack 参数
let shell = require('shelljs');

let script = 'webpack-dev-server --open-page='+ indexPage +' --inline --hot --config build/webpack.dev.conf.js'
shell.exec(script)

添加路径别名

alias: {
  '@': resolve('src'),
  'pages': resolve('src/pages'),
  'components': resolve('src/components'),
  'services': resolve('src/services')
}
import { gtagPV } from '@/utils/gtag';

不同环境自动适配域名

原则上不允许硬编码域名,直接写路径 path,或者引入以下 JS 统一处理

// 前端URL,直接写路径
window.location.href = '/helloworld/help/index.html'

// 后端接口
import conf from 'services/config'
console.log(conf.host)

webpack 在打包时会自动对域名相关信息进行替换。

如何实现

  1. 针对不同环境,使用不同的配置文件,比如:config.js,config-qa.js

保证所有配置文件导出的参数名保持一致

// dev 测试环境配置
let baseUrl = 'dev.xxx.com'  // 服务端接口环境
let host = '//'+ window.location.hostname  // 前端网页域名

export default {
  baseUrl,
  host
}
  1. webpack 通过 NormalModuleReplacementPlugin 插件完成替换
const prodWebpackConfig = merge(webpackConfig, {
    plugins: [
      new webpack.NormalModuleReplacementPlugin(
        /services\/config\.js/, // services 目录默认配置文件
        './config-qa.js'  // 替换配置文件
      )
    ]
  })

接口请求完善

  1. 编写通用拦截器
// 请求拦截器
axios.interceptors.request.use(function (config) {
  // 自动添加 token 之类的通用参数
  return config;
}, function (error) {
  return Promise.reject(error);
});

// 响应拦截器
axios.interceptors.response.use(function (response) {
  // 处理一些通用响应,比如 200,401之类
  return response;
}, function (error) {
  return Promise.reject(error);
});

使用时引入一次拦截器,发起请求同 axios API

import axios from 'services/axios'
axios.get('/api?param=value')
.then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});
  1. 请求方法封装
import { postFromData } from 'services/axios'
// postFromData:post 请求以 form 表单形式发送数据,默认为 JSON
postFromData.then((response) => {
    console.log(response);
}).catch((error) => {
    console.log(error);
})

服务器部署优化

优化效果

mobile-intl-deploy

# 单独打包
npm run buildDev        // 打 dev 环境包
npm run buildQa         // 打 QA 环境包
npm run buildPre        // 打仿真环境包
npm run buildOnline     // 打线上环境包

# 单独部署
npm run dev         // 部署 dev 测试环境
npm run qa          // 部署 QA 测试环境
npm run pre         // 部署仿真测试环境
npm run online      // 部署线上环境

# 打包 + 部署快捷方式
# 第一个参数为打包所用的 server 接口环境,第二个参数为服务器部署环境
# 两个参数的可选值均为:dev || qa || pre || online
# 调用方式一
npm run deploy -- --buildEnv=dev --deployEnv=qa
# 调用方式二
npm run deploy -- dev qa

如何实现

  1. 创建通用的 webpack 配置:webpack.cust.conf.js,处理各个环境的打包逻辑
// exports function 能接收参数
module.exports = function(env, argv){
  // 根据 npm run script 传入的参数判断环境
  let apiEnv = env.serverApi || 'dev'
  if (apiEnv === 'dev'){
    // 做点啥
  }
}
  1. npm script 传入 webpack 参数:serverApi,实现环境自定义
"scripts": {
  "buildDev": "webpack --env.serverApi dev --mode production --progress --color --config build/webpack.cust.conf.js",
  "buildQa": "webpack --env.serverApi qa --mode production --progress --color --config build/webpack.cust.conf.js",
}
  1. npm script 传入 node 参数:env,实现部署服务器自定义
"scripts": {
  "dev": "node deploy.js --env=dev --color=always",
  "qa": "node deploy.js --env=qa --color=always",
}
// deploy.js 为部署脚本
let envParam = process.argv[2].slice(6)  // 裁掉 --env= 获取环境参数
let deployEnv = envParam || 'dev' // 服务器环境

初始化项目脚手架

初始化项目结构

  • 单页项目模板 /src/pages/singlePage
  • 多页项目模板 /src/pages/multiPage

根据提问完成项目初始化

npm run initApp

如何实现

并没用Yeoman

关键工具

提问交互

const askQuestion = async () => {
  // 模板类型
  let { templateType } = await inquirer.prompt([
    {
      type: 'list',
      name: 'templateType',
      message: 'Which template do you want?',
      choices: [
        {
          name: 'SPA (Single page)',
          value: 'SPA'
        }
      ]
    }
  ])

  // 项目名称
  let { projectName } = await inquirer.prompt([
    {
      name: 'projectName',
      message: 'What\'s the project name',
    }
  ])
}

askQuestion().catch((err) => {
  console.log('init project err: ', err)
})

拷贝工程模板

let shell = require('shelljs');
let sourcePath = '../src/pages/singlePage'
shell.cp('-R', sourcePath, '../src/pages/newProject');

打包时排除模板目录

glob.sync('./src/pages/**/app.js', {
  // 忽略项目模板目录
  ignore: ['./src/pages/singlePage/**', './src/pages/multiPage/**']
}).forEach(path => {
  // do something
})

独立脚手架模板

可以利用 download-git-repo,把项目模板从 github 下载下来

命令行日志美化

关键工具

遇到的坑

使用 shelljs 库执行的时候,命令行的样式失效,issue 在此

解决办法

强制启用命令行样式

  • webpack 加 --color
webpack --env.serverApi dev --mode production --progress --color --config build/webpack.cust.conf.js
  • Node 加 --color=always
cd ./tools && node deploy.js --env=dev --color=always

自定义日志输出样式

参考了 signale 的样式,直接用它也没问题

const chalk = require('chalk');
// log with chalk
const chalkLog = function(options){
  options = options || {
    color: 'red',  // chalk color
    label: 'my-project',
    badge: '🎉',
    tag: 'tag',
    msg: 'some log msg',
  }
  let logLabel = options.label || 'my-project'
  let logColor = options.color || 'black'
  const logTxt = chalk.gray('['+ logLabel +'] > ') + chalk[logColor](''+ options.badge +' ') + chalk.underline(chalk[logColor](options.tag)) +'  '+ options.msg

  console.log(logTxt)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions