开开心心撸一个VUE转小程序原生的Webpack-loader(上)

4,263 阅读6分钟

喜迎中华人民共和国成立70周年~~~ 🎵我和我的祖国,一刻都不能分割🎵

前段时间接到一个项目改造任务,需要把Vue项目里的功能重写成原生小程序。(惊不惊喜,意不意外)

在公司里,这种功能保持不变但是需要写在不同平台上的问题,其实很多,我们不可能换一次平台,手写一次,耗时耗力在重复性高的工作中,难免会有错误。

最近看了看如何撸一个webpack-loader灵机一动,我们也可以写一个啊,还是用vue项目开发,打包时生成小程序的文件。这里,现在只开发到vue文件转小程序原生,css相对于简单很多只需要px转2px。而js就复杂很多了,放在Future完成了,有兴趣的小伙伴也可以加入一起开发啊~一起完成它~

为什么用webpack-laoder?

loader 用于对模块的源代码进行转换。loader可以使你在 import或"加载"模块时预处理文件。因此,loader类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。 ---摘自官方文档

在这里我用loader,是因为他有这么几个优点:

  • loader 接收查询参数。用于对 loader 传递配置。
  • loader 也能够使用 options 对象进行配置。
  • loader 运行在 Node.js 中,并且能够执行任何可能的操作。

接受参数我们可以自定义最后生成成什么平台的小程序。预处理,我们可以在打包前拿到文件内容利用vue的插件解析出templatecssjavascript三个文件,从而进一步取处理。

第一节,我们先把项目搭起来~ let't Go !

在vue项目中搭起环境

我自己在项目中创建了一个task文件,专门用来开发这个webpack-laoder,自己简单的配置了webpack。

接下俩简单介绍一下,里面3个文件的作用。

build.js

build.js用途就是调用wenpack输出我们小程序文件

const shell = require('shelljs') 
const { resolve } = require('path')
const fs = require('fs')
const webpack = require('webpack')
const _ = require('lodash')
const r = url => resolve(process.cwd(), url)
const config = require('./config') //我们抽出的小程序主要的文件
const webpackConf = require('./webpack.conf') // webpack配置

const assetsPath = config.assetsPath

shell.rm('-rf', assetsPath)
shell.mkdir(assetsPath)

const renderConf = webpackConf
const entry = () => _.reduce(config.json.pages, (en, i) => {
  en[i] = resolve(__dirname, '../src/components/', `HelloWorld.vue`) //需要转成小程序的文件的地址
  return en
}, {}) //输出一个对象

renderConf.output = { //输出文件的配置
  path: config.assetsPath,
  filename: '[name].js'
}

renderConf.entry = entry()
// renderConf.entry.app = config.app

// 如果你不传入回调函数到 webpack 执行函数中,就会得到一个 webpack Compiler 实例。你可以通过它手动触发 webpack 执行器,或者是让它执行构建并监听变更。和 CLI API 很类似。Compiler 实例提供了以下方法
const compiler = webpack(renderConf) //导入的 webpack 函数需要传入一个 webpack 配置对象,当同时传入回调函数时就会执行 webpack 

fs.writeFileSync(resolve(config.assetsPath, './app.json'), JSON.stringify(config.json), 'utf8')//一步写入文件小程序的app.json
   
const callback = (err, stats) => {
    console.log('Compiler 已经完成执行。');
    if (err) process.stdout.write(err) //如果有报错,就在控制台打印出报错
};

compiler.run(callback)

webpack.conf.js

webpack的配置项

const { resolve } = require('path') //方法会把一个路径或路径片段的序列解析为一个绝对路径。
const r = url => resolve(__dirname, url)
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin') //将单个文件或整个目录复制到构建目录
const ProgressBarPlugin = require('progress-bar-webpack-plugin') //打包时候在命令行里的进度条
const ExtractTextPlugin = require('extract-text-webpack-plugin') //打包的时候分离出文本,比如css,打包到单独的文件夹里

const extractSass = new ExtractTextPlugin({
  filename: '[name].wxss'
})

const config = require('../config')

module.exports = {
  devtool: false,
  output: {
    path: config.assetsPath,
    filename: '[name].js'
  },
  resolve: {
    alias: {
      utils: r('../utils/utils')
    }
  },
  resolveLoader: {
    // 去哪些项目下寻找Loader,有先后顺序之分,这里是为了本地调试laoder方便
    modules: ['node_modules', './loaders/'],
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: [
            'latest'
          ]
        }
      },
      {
        test: /\.vue$/,
        loader: 'program-loader', //这个就是我们变成的小程序laoder
        options: {
          dist: './program',
          type:''
        }
      }
    ]
  },
  plugins: [
    extractSass,
    new CopyWebpackPlugin([
      {
        from : {
          glob: 'pages/**/*.json',
          to: ''
        } 
      }, {
        from: 'static',
        to: 'static'
      }
    ]),
    new webpack.optimize.ModuleConcatenationPlugin(),//解释是启用作用域提升,webpack3新特性,作用是让代码文件更小、运行的更快
    new ProgressBarPlugin()
  ]
}

config.js

这个文件是配置小程序生成的json文件,公共style等。

const { resolve } = require('path')
const r = url => resolve(__dirname, url)
const assetsPath = resolve(process.cwd(), './program') 

module.exports={
    "json":{ //通过这个配置小程序的页面
        "pages":[
          "pages/home/home",
        ],
        "window":{
        }
    },
    "style":{
        url:r('./style/common.less'),
        lang:'less'
    },
    "assetsPath": assetsPath,
}

运行就靠一行代码 node task/build.js 到这里vue项目里的环境配置就做好了,这里你需要知道一点本地调试wenpack-loader的知识 加载调试本地loader

program-loader

接下来,我们将具体解析program-loader里的代码,这里我将内容分成3个部分,第一部分介绍如何解析vue文件(最简单的一部分),第二部分解析将vue中的template解析成一个nodeTree,第三部分也是比较重要的将nodeTree再变成小程序中的原生标签。

所以,我们开始第一部分吧。

先看一下program-loader的项目结构

render-css.js用于解析样式文件,render-html.js用于解析html标签,render-script.js解析js文件。runfile.js用来输出html的文件,template.js用来解析nodeTree转成小程序原生标。 index.js启动文件。

index.js用到一个解析vue文件的插件vue-template-compiler可以将vue分为html,css,script。同时,我们还可以利用loader的异步性和缓存机制充分的来提高编译效率。

const renderHtml = require('./lib/render-html')
const renderCss = require('./lib/render-css')
const renderScript = require('./lib/render-script')
const parseTemplate = require('./lib/template')
const runfile = require('./lib/runfile')
const {
  parseComponent
} = require('vue-template-compiler')
module.exports = function (content) {
  this.cacheable() //缓存 webpack充分地利用缓存来提高编译效率
  var cb = this.async() // 异步 当一个 Loader 无依赖,可异步的时候我想都应该让它不再阻塞地去异步
  const parts = parseComponent(content) //解析vue的格式
  let { template= {} } = parts
  
  if (template) {
    try{
      let nodes = renderHtml.parse(template.content)
      let wxml = parseTemplate.outputWxml(nodes)
      runfile.call(this, wxml)  
    }
    catch(err){
      console.log(err)
    }

  }
  if (parts.styles && parts.styles.length) {
    renderCss.call(this, parts.styles[0])  
  }
  if (parts.script) {
    renderScript.call(this, parts.script, cb)
    return
  } else {
    cb(null, '')
  }
}

render-css.js解析样式的时候,我用less转成css,当然这里并不限制你用什么来编写,你记得lang标示出你用的语言就可以~ 然后利用px2rpx去转rpx。这里很简单,你很容易就能看懂。

const loaderUtils = require('loader-utils')
const fs = require('fs-extra')
const { resolve } = require('path')
const Px2rpx = require('px2rpx');
const px2rpxIns = new Px2rpx({ rpxUnit: 0.5 });

const con = {
  stylus: (file, data) => new Promise(resolve => {
    require('stylus').render(data, { filename: file }, (err, css) => {
      if (err) throw err
        
      resolve(css)
    }) 
  }),
  less: (file, data) => new Promise(resolve => {
    require('less').render(data, {}, (err, result) => {
      if (err) throw err
      let css = result.css
      resolve(px2rpxIns.generaterpx(css))
    }) 
  }),
  scss: (file, data) => new Promise(resolve => {
    require('node-sass').render({
      file, 
      data,
      outputStyle: 'compressed'
    }, (err, result) => {
      if (err) throw err

      resolve(result.css)
    }) 
  }),
  sass: (file, data) => new Promise(resolve => {
    require('node-sass').render({
      file, 
      data,
      outputStyle: 'compressed',
      indentedSyntax: true
    }, (err, result) => {
      if (err) throw err

      resolve(result.css)
    }) 
  })
}


module.exports = async function (style) {
  this.cacheable()


  const options = loaderUtils.getOptions(this)
  const file = options.type === 'wx' ? "[name].wxss" :"[name].acss" ;
  const pullPath = loaderUtils.interpolateName(this, `[path][name].program`, options)
  const filename = loaderUtils.interpolateName(this, file, options)
  const folder = loaderUtils.interpolateName(this, `[folder]`, options)
  const dist = options.dist || 'dist'

  let stylesheet = style.content
  let lang = style.lang
  if (lang) {
    const render = con[style.lang]

    stylesheet = await render(pullPath, stylesheet)
  }
  fs.outputFileSync(resolve(process.cwd(), `${dist}/pages/${folder}/${filename}`), stylesheet)

  return ``
}


render-script.js我没有做什么操作,就是编译后输出了。这里还没有去研究 = =|||。

const { transform } = require('babel-core')

module.exports = function (script, cb) {
  this.cacheable()
  // 获取当前用户给当前loader传入的参数对象options
  cb(null, script.content)
}

到这里,第一部分应该没啥问题了,接下来,我们要进入最重要的第二部分了~

稍等,因为篇幅较长,我分成了上下段。等我。。。就回来。。。