webpack实现构建多个个性化多页应用

543 阅读3分钟

需求

  前段时间,公司希望开发多个基础功能,页面结构相同、局部ui,布局,小功能不相同的多页web应用。再眼前两种方案可选,第一种方案:每个web应用持有一份相同代码,然后针对不同的web应用修改各自的代码。这种方式相对比较简单,只需简单点的ctrl+c、ctrl+v、修改各自代码就完事了,然而这种方式在后期的代码维护比较困难,当需要修复某个共有的bug或者增添某个共有的功能模块时,你需要对所有的web应用进行修改,所以就有了第二种方案:所有的web应用共用一套代码,利用webpack控制各个web应用的不同点进行选择性编译。(控制web应用选择性编译有很多种方法如一些样式基调(主色调、辅色、字体大小、标志性背景图...)或者应用种特有文本(应用名)、功能块的开关等吗,可以通过为每个应用建立配置文件(样式类可以用预编译语言sass、less建立配置文件,文本功能块可以通过js、json建立配置文件),这些方法比较简单便不再累述,本文主要介绍如何处理一些差异较大如整个页面布局(html)或者整个页面样式(css)或者整个页面数据逻辑(js)不同的web应用)

实现

思路

  首先新建一个template文件夹存放模板应用html、css、js文件,再为每个应用创建一个私有文件夹用来存放为定制个性化取代模板同名的html、css、js,配置webpack优先读取私有目录下的文件再读取模板文件里那些私有文件没有的文件。

html、js

html、css相对比较简单,主要webpack配置如下


module.exports = env => {
  // 其他代码
  ...
  // 模板文件地址
  const tmpDir = './src/template';
  // 编译html入口目录
  const htmlDir = `./src/${project}/html`;
  // 编译es6入口目录
  const es6Dir = `./src/${project}/script`;
  /**
   * 获取文件对象
   * @param  {string} filePath 文件路径正则
   * @return {Object}          匹配文件对象
   * 文件对象格式{[文件名]: [对应文件路径]}
   */
  const generateEntry = filePath => glob.sync(filePath).reduce((res, ret) => {
    // 去除文件路径中的文件夹路径和文件后缀名
    let filename = ret.slice(ret.lastIndexOf('/') + 1, ret.lastIndexOf('.'));
    // 判断是否在不编译文件数组中
    noreFile.includes(filename) ? '' : res[filename] = ret;
    return res;
  }, {});
  /**
   * 获取es6入口文件地址对象
   * @return {object} 入口文件地址对象
   * 返回对象格式为:{[js文件名]: [对应路径]}
   * 详情可查看: https://webpack.docschina.org/configuration/entry-context/#entry
   */
  const entry = Object.assign(generateEntry(`${tmpDir}/script/*.js`), generateEntry(`${es6Dir}/*.js`));
  // 生成html插件数组
  // 格式:[new HtmlWebpackPlugin([html-webpack-plugin[配置])]
  // 详情可查看: https://github.com/jantimon/html-webpack-plugin
  const htmlConfig = (() => {
    let res = [];
    let obj = Object.assign(generateEntry(`${tmpDir}/html/*.html`), generateEntry(`${htmlDir}/*.html`));
    for (let key in obj) {
      res.push(new HtmlWebpackPlugin({
        // 生成html路径
        filename: path.resolve(__dirname, 'html', `${key}.html`),
        // 目标html路径
        template: obj[key],
        // 其他代码
        ...
      }))
    }
    return res;
  })();
  // 其他代码
  ...
  return {
    // 引入编译js文件
    entry,
    plugins: [
      // 其他配置
      ...
    ].concat(htmlConfig),
    // 其他配置
    ...
  }
}

其中变量project为通过env传入的web应用的私有文件夹名。本段代码主要用glob模块分别读取模板文件和应用的私有文件目录,再由Object.assign方法将其合并交由webpack编译打包。

css

  由css文件不是直接通过webpack引入,而是由js文件引入,所以需要在js编译前对文件进行分析,动态生成css注入语句,这里采用自定义loader,为了编程方便,设定引入的js和css的文件名相同,代码如下

// 引入获取配置函数
const loaderUtils = require('loader-utils');
// 预编译对象
var precompliedObj;

module.exports = function (source) {
  if (!precompliedObj) {
    // 获取预编译对象
    let options = loaderUtils.getOptions(this);
    // 引入预编译文件,并把初始化参数传入预编译文件
    precompliedObj = require(options.nodeFile)(options.param);
  }
  // 获取编译资源名
  let filename = this.resourcePath.slice(this.resourcePath.lastIndexOf('\\') + 1, this.resourcePath.lastIndexOf('.'));
  // 匹配 // ##函数名## 和 ##函数名## 为预编译锚点
  return source.replace(/(\/\/\s*)?##(\w+)(\(.*\))?##\s*/g, function (str) {
    let fn = /##(.+)##/.exec(str)[1];
    // 获取函数名
    let key;
    let index = fn.indexOf('(');
    if (index === -1) {
      key = fn;
      fn = ` precompliedObj.${fn}('${filename}')`;
    } else {
      key = fn.slice(0, index);
      fn = `precompliedObj.${key}.bind(precompliedObj, '${filename}')${fn.slice(index)}`
    }
    // 在预编译node文件中调用预编译函数并注入预编译锚点
    return !!precompliedObj[key] ? eval(fn) : str;
  });
}

这个loader主要在js编译前根据**// ##函数名## 和 ##函数名##**调用node函数,其中调用loader时必须存入将要调用nodejs文件路径,其他参数会和文件名一同传入node函数中,webpack引入loader配置如下


 // 用babel解析js文件
{
  test: /\.js?$/,
  include: [path.resolve(__dirname, 'src')],
  use: ['babel-loader', {
    loader: 'precompiled-loader',
    options: {
      // node预编译文件路径
      nodeFile: path.resolve(__dirname, 'node.dev.js'),
      // 导入编译文件初始化参数
      param: {
        // 出入应用私有文件名
        project
      }
    }
  }]
}

node.dev.js函数定义如下


// 私有有样式文件名数组
var privateStyle;

// 获取路径下的文件名数组
const getFileArray = path => glob.sync(path).map(item => item.slice(item.lastIndexOf('/') + 1, item.lastIndexOf('.')));

module.exports = function (obj) {
  // 获取私有有样式文件名数组
  !privateStyle && (privateStyle = getFileArray(`./src/${obj.project}/style/*.scss`));
  return {
    // 引入样式
    importStyle: function (filename) {
      return `\
        import '${this.style(filename)}';\
      `;
    }
  }
}

代码主要通过判断传入的文件名是否在应用的私有目录下,如果存在则返回项目的私有目录下对应的文件路径,如果不出在则返回模板目录下的文件路径。最后只需要在js中调用importStyle函数就行了。


// 引入样式
// 注下面一行不是注释  不可删除  为样式导入锚点
// ##importStyle##

扩展

  通过上面介绍的css动态引入方法,我们可以进一步挖掘个性化编译,局部个性化编译:有些页面可能只有某些模块不同,可以把应用不同点封装成组件,通过动态引入组件的方式,编译出不同个性的应用。